Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7d57dd1e5 | ||
|
|
d9b1b4104f | ||
|
|
b445fa1974 | ||
|
|
f265d1f5e2 | ||
|
|
a05a5e6db8 | ||
|
|
8c55e5b4f3 | ||
|
|
477a52e779 | ||
|
|
107823fc2c | ||
|
|
621083fe1f | ||
|
|
0770238718 | ||
|
|
c01deb5576 | ||
|
|
82acb6fc72 | ||
|
|
8bb61593b2 | ||
|
|
3ebf924651 | ||
|
|
46a3b7e94d |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Create universal macOS Electron applications
|
> Create universal macOS Electron applications
|
||||||
|
|
||||||
[](https://circleci.com/gh/electron/universal)
|
[](https://circleci.com/gh/electron/universal)
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -30,3 +30,7 @@ because it contains two apps in one.
|
|||||||
The way `@electron/universal` works today means you don't need to worry about
|
The way `@electron/universal` works today means you don't need to worry about
|
||||||
things like building universal versions of your native modules. As long as
|
things like building universal versions of your native modules. As long as
|
||||||
your x64 and arm64 apps work in isolation the Universal app will work as well.
|
your x64 and arm64 apps work in isolation the Universal app will work as well.
|
||||||
|
|
||||||
|
#### How do I build my app for Apple silicon in the first place?
|
||||||
|
|
||||||
|
Check out the [Electron Apple silicon blog post](https://www.electronjs.org/blog/apple-silicon)
|
||||||
|
|||||||
7
entry-asar/has-asar.js
Normal file
7
entry-asar/has-asar.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
if (process.arch === 'arm64') {
|
||||||
|
process._archPath = require.resolve('../app-arm64.asar');
|
||||||
|
} else {
|
||||||
|
process._archPath = require.resolve('../app-x64.asar');
|
||||||
|
}
|
||||||
|
|
||||||
|
require(process._archPath);
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
if (process.arch === 'arm64') {
|
|
||||||
process._asarPath = require.resolve('../arm64.app.asar');
|
|
||||||
} else {
|
|
||||||
process._asarPath = require.resolve('../x64.app.asar');
|
|
||||||
}
|
|
||||||
|
|
||||||
require(process._asarPath);
|
|
||||||
7
entry-asar/no-asar.js
Normal file
7
entry-asar/no-asar.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
if (process.arch === 'arm64') {
|
||||||
|
process._archPath = require.resolve('../app-arm64');
|
||||||
|
} else {
|
||||||
|
process._archPath = require.resolve('../app-x64');
|
||||||
|
}
|
||||||
|
|
||||||
|
require(process._archPath);
|
||||||
14
package.json
14
package.json
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/*",
|
"dist/*",
|
||||||
|
"entry-asar/*",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"author": "Samuel Attard",
|
"author": "Samuel Attard",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@continuous-auth/semantic-release-npm": "^2.0.0",
|
"@continuous-auth/semantic-release-npm": "^2.0.0",
|
||||||
|
"@types/debug": "^4.1.5",
|
||||||
"@types/fs-extra": "^9.0.4",
|
"@types/fs-extra": "^9.0.4",
|
||||||
"@types/node": "^14.14.7",
|
"@types/node": "^14.14.7",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.0",
|
||||||
@@ -37,6 +39,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@malept/cross-spawn-promise": "^1.1.0",
|
"@malept/cross-spawn-promise": "^1.1.0",
|
||||||
"asar": "^3.0.3",
|
"asar": "^3.0.3",
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"dir-compare": "^2.4.0",
|
||||||
"fs-extra": "^9.0.1"
|
"fs-extra": "^9.0.1"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.ts": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/asar-utils.ts
Normal file
21
src/asar-utils.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { d } from './debug';
|
||||||
|
|
||||||
|
export enum AsarMode {
|
||||||
|
NO_ASAR,
|
||||||
|
HAS_ASAR,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const detectAsarMode = async (appPath: string) => {
|
||||||
|
d('checking asar mode of', appPath);
|
||||||
|
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(asarPath))) {
|
||||||
|
d('determined no asar');
|
||||||
|
return AsarMode.NO_ASAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
d('determined has asar');
|
||||||
|
return AsarMode.HAS_ASAR;
|
||||||
|
};
|
||||||
3
src/debug.ts
Normal file
3
src/debug.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import * as debug from 'debug';
|
||||||
|
|
||||||
|
export const d = debug('electron-universal');
|
||||||
70
src/file-utils.ts
Normal file
70
src/file-utils.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
|
||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const MACHO_PREFIX = 'Mach-O ';
|
||||||
|
|
||||||
|
export enum AppFileType {
|
||||||
|
MACHO,
|
||||||
|
PLAIN,
|
||||||
|
SNAPSHOT,
|
||||||
|
APP_CODE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppFile = {
|
||||||
|
relativePath: string;
|
||||||
|
type: AppFileType;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param appPath Path to the application
|
||||||
|
*/
|
||||||
|
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||||
|
const files: AppFile[] = [];
|
||||||
|
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const traverse = async (p: string) => {
|
||||||
|
p = await fs.realpath(p);
|
||||||
|
if (visited.has(p)) return;
|
||||||
|
visited.add(p);
|
||||||
|
|
||||||
|
const info = await fs.stat(p);
|
||||||
|
if (info.isSymbolicLink()) return;
|
||||||
|
if (info.isFile()) {
|
||||||
|
let fileType = AppFileType.PLAIN;
|
||||||
|
|
||||||
|
var fileOutput = '';
|
||||||
|
try {
|
||||||
|
fileOutput = await spawn('file', ['--brief', '--no-pad', p]);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ExitCodeError) {
|
||||||
|
/* silently accept error codes from "file" */
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p.includes('app.asar')) {
|
||||||
|
fileType = AppFileType.APP_CODE;
|
||||||
|
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
||||||
|
fileType = AppFileType.MACHO;
|
||||||
|
} else if (p.endsWith('.bin')) {
|
||||||
|
fileType = AppFileType.SNAPSHOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
relativePath: path.relative(appPath, p),
|
||||||
|
type: fileType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.isDirectory()) {
|
||||||
|
for (const child of await fs.readdir(p)) {
|
||||||
|
await traverse(path.resolve(p, child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await traverse(appPath);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
264
src/index.ts
264
src/index.ts
@@ -1,11 +1,13 @@
|
|||||||
import { spawn } from '@malept/cross-spawn-promise';
|
import { spawn } from '@malept/cross-spawn-promise';
|
||||||
import * as asar from 'asar';
|
import * as asar from 'asar';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as dircompare from 'dir-compare';
|
||||||
const MACHO_PREFIX = 'Mach-O ';
|
import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
|
||||||
|
import { AsarMode, detectAsarMode } from './asar-utils';
|
||||||
|
import { sha } from './sha';
|
||||||
|
import { d } from './debug';
|
||||||
|
|
||||||
type MakeUniversalOpts = {
|
type MakeUniversalOpts = {
|
||||||
/**
|
/**
|
||||||
@@ -28,78 +30,12 @@ type MakeUniversalOpts = {
|
|||||||
force: boolean;
|
force: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AsarMode {
|
const dupedFiles = (files: AppFile[]) =>
|
||||||
NO_ASAR,
|
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
|
||||||
HAS_ASAR,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const detectAsarMode = async (appPath: string) => {
|
|
||||||
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR;
|
|
||||||
|
|
||||||
return AsarMode.HAS_ASAR;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum AppFileType {
|
|
||||||
MACHO,
|
|
||||||
PLAIN,
|
|
||||||
SNAPSHOT,
|
|
||||||
APP_CODE,
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppFile = {
|
|
||||||
relativePath: string;
|
|
||||||
type: AppFileType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllFiles = async (appPath: string): Promise<AppFile[]> => {
|
|
||||||
const files: AppFile[] = [];
|
|
||||||
|
|
||||||
const visited = new Set<string>();
|
|
||||||
const traverse = async (p: string) => {
|
|
||||||
p = await fs.realpath(p);
|
|
||||||
if (visited.has(p)) return;
|
|
||||||
visited.add(p);
|
|
||||||
|
|
||||||
const info = await fs.stat(p);
|
|
||||||
if (info.isSymbolicLink()) return;
|
|
||||||
if (info.isFile()) {
|
|
||||||
let fileType = AppFileType.PLAIN;
|
|
||||||
|
|
||||||
const fileOutput = await spawn('file', ['--brief', '--no-pad', p]);
|
|
||||||
if (p.includes('app.asar')) {
|
|
||||||
fileType = AppFileType.APP_CODE;
|
|
||||||
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
|
||||||
fileType = AppFileType.MACHO;
|
|
||||||
} else if (p.endsWith('.bin')) {
|
|
||||||
fileType = AppFileType.SNAPSHOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
relativePath: path.relative(appPath, p),
|
|
||||||
type: fileType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.isDirectory()) {
|
|
||||||
for (const child of await fs.readdir(p)) {
|
|
||||||
await traverse(path.resolve(p, child));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await traverse(appPath);
|
|
||||||
|
|
||||||
return files;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dupedFiles = (files: AppFile[]) => files.filter(f => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
|
|
||||||
|
|
||||||
const sha = async (filePath: string) => {
|
|
||||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
||||||
|
d('making a universal app with options', opts);
|
||||||
|
|
||||||
if (process.platform !== 'darwin')
|
if (process.platform !== 'darwin')
|
||||||
throw new Error('@electron/universal is only supported on darwin platforms');
|
throw new Error('@electron/universal is only supported on darwin platforms');
|
||||||
if (!opts.x64AppPath || !path.isAbsolute(opts.x64AppPath))
|
if (!opts.x64AppPath || !path.isAbsolute(opts.x64AppPath))
|
||||||
@@ -110,17 +46,21 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
|||||||
throw new Error('Expected opts.outAppPath to be an absolute path but it was not');
|
throw new Error('Expected opts.outAppPath to be an absolute path but it was not');
|
||||||
|
|
||||||
if (await fs.pathExists(opts.outAppPath)) {
|
if (await fs.pathExists(opts.outAppPath)) {
|
||||||
|
d('output path exists already');
|
||||||
if (!opts.force) {
|
if (!opts.force) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The out path "${opts.outAppPath}" already exists and force is not set to true`,
|
`The out path "${opts.outAppPath}" already exists and force is not set to true`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
d('overwriting existing application because force == true');
|
||||||
await fs.remove(opts.outAppPath);
|
await fs.remove(opts.outAppPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const x64AsarMode = await detectAsarMode(opts.x64AppPath);
|
const x64AsarMode = await detectAsarMode(opts.x64AppPath);
|
||||||
const arm64AsarMode = await detectAsarMode(opts.arm64AppPath);
|
const arm64AsarMode = await detectAsarMode(opts.arm64AppPath);
|
||||||
|
d('detected x64AsarMode =', x64AsarMode);
|
||||||
|
d('detected arm64AsarMode =', arm64AsarMode);
|
||||||
|
|
||||||
if (x64AsarMode !== arm64AsarMode)
|
if (x64AsarMode !== arm64AsarMode)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -128,23 +68,28 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
|||||||
);
|
);
|
||||||
|
|
||||||
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
|
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
|
||||||
|
d('building universal app in', tmpDir);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
d('copying x64 app as starter template');
|
||||||
const tmpApp = path.resolve(tmpDir, 'Tmp.app');
|
const tmpApp = path.resolve(tmpDir, 'Tmp.app');
|
||||||
await spawn('cp', ['-R', opts.x64AppPath, tmpApp]);
|
await spawn('cp', ['-R', opts.x64AppPath, tmpApp]);
|
||||||
|
|
||||||
const uniqueToX64: string[] = [];
|
const uniqueToX64: string[] = [];
|
||||||
const uniqueToArm64: string[] = [];
|
const uniqueToArm64: string[] = [];
|
||||||
const x64Files = await getAllFiles(await fs.realpath(tmpApp));
|
const x64Files = await getAllAppFiles(await fs.realpath(tmpApp));
|
||||||
const arm64Files = await getAllFiles(opts.arm64AppPath);
|
const arm64Files = await getAllAppFiles(await fs.realpath(opts.arm64AppPath));
|
||||||
|
|
||||||
for (const file of dupedFiles(x64Files)) {
|
for (const file of dupedFiles(x64Files)) {
|
||||||
if (!arm64Files.some(f => f.relativePath === file.relativePath)) uniqueToX64.push(file.relativePath);
|
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
|
||||||
|
uniqueToX64.push(file.relativePath);
|
||||||
}
|
}
|
||||||
for (const file of dupedFiles(arm64Files)) {
|
for (const file of dupedFiles(arm64Files)) {
|
||||||
if (!x64Files.some(f => f.relativePath === file.relativePath)) uniqueToArm64.push(file.relativePath);
|
if (!x64Files.some((f) => f.relativePath === file.relativePath))
|
||||||
|
uniqueToArm64.push(file.relativePath);
|
||||||
}
|
}
|
||||||
if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) {
|
if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) {
|
||||||
|
d('some files were not in both builds, aborting');
|
||||||
console.error({
|
console.error({
|
||||||
uniqueToX64,
|
uniqueToX64,
|
||||||
uniqueToArm64,
|
uniqueToArm64,
|
||||||
@@ -154,59 +99,160 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of x64Files.filter(f => f.type === AppFileType.PLAIN)) {
|
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) {
|
||||||
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
|
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
|
||||||
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
|
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
|
||||||
if (x64Sha !== arm64Sha) {
|
if (x64Sha !== arm64Sha) {
|
||||||
console.error(`${x64Sha} !== ${arm64Sha}`);
|
d('SHA for file', file.relativePath, `does not match across builds ${x64Sha}!=${arm64Sha}`);
|
||||||
throw new Error(`Expected all non-binary files to have identical SHAs when creating a universal build but "${file.relativePath}" did not`);
|
throw new Error(
|
||||||
|
`Expected all non-binary files to have identical SHAs when creating a universal build but "${file.relativePath}" did not`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const machOFile of x64Files.filter(f => f.type === AppFileType.MACHO)) {
|
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
|
||||||
|
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath));
|
||||||
|
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath));
|
||||||
|
|
||||||
|
d('joining two MachO files with lipo', {
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
});
|
||||||
await spawn('lipo', [
|
await spawn('lipo', [
|
||||||
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
first,
|
||||||
await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)),
|
second,
|
||||||
'-create',
|
'-create',
|
||||||
'-output',
|
'-output',
|
||||||
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we don't have an ASAR we need to check if the two "app" folders are identical, if
|
||||||
|
* they are then we can just leave one there and call it a day. If the app folders for x64
|
||||||
|
* and arm64 are different though we need to rename each folder and create a new fake "app"
|
||||||
|
* entrypoint to dynamically load the correct app folder
|
||||||
|
*/
|
||||||
if (x64AsarMode === AsarMode.NO_ASAR) {
|
if (x64AsarMode === AsarMode.NO_ASAR) {
|
||||||
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'x64.app'));
|
d('checking if the x64 and arm64 app folders are identical');
|
||||||
await fs.copy(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'arm64.app'));
|
const comparison = await dircompare.compare(
|
||||||
} else {
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
|
||||||
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'x64.app.asar'));
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
|
||||||
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
|
{ compareSize: true, compareContent: true },
|
||||||
if (await fs.pathExists(x64Unpacked)) {
|
);
|
||||||
await fs.move(x64Unpacked, path.resolve(tmpApp, 'Contents', 'Resources', 'x64.app.asar.unpacked'));
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.copy(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'arm64.app.asar'));
|
if (!comparison.same) {
|
||||||
const arm64Unpacked = path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar.unpacked');
|
d('x64 and arm64 app folders are different, creating dynamic entry ASAR');
|
||||||
if (await fs.pathExists(arm64Unpacked)) {
|
await fs.move(
|
||||||
await fs.copy(arm64Unpacked, path.resolve(tmpApp, 'Contents', 'Resources', 'arm64.app.asar.unpacked'));
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'),
|
||||||
|
);
|
||||||
|
await fs.copy(
|
||||||
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
||||||
|
await fs.mkdir(entryAsar);
|
||||||
|
await fs.copy(
|
||||||
|
path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'),
|
||||||
|
path.resolve(entryAsar, 'index.js'),
|
||||||
|
);
|
||||||
|
let pj = await fs.readJson(
|
||||||
|
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
|
||||||
|
);
|
||||||
|
pj.main = 'index.js';
|
||||||
|
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||||
|
await asar.createPackage(
|
||||||
|
entryAsar,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
d('x64 and arm64 app folders are the same');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
/**
|
||||||
await fs.mkdir(entryAsar);
|
* If we have an ASAR we just need to check if the two "app.asar" files have the same hash,
|
||||||
await fs.copy(path.resolve(__dirname, '..', '..', 'entry-asar', 'index.js'), path.resolve(entryAsar, 'index.js'));
|
* if they are, same as above, we can leave one there and call it a day. If they're different
|
||||||
let pj: any;
|
* we have to make a dynamic entrypoint. There is an assumption made here that every file in
|
||||||
if (x64AsarMode === AsarMode.NO_ASAR) {
|
* app.asar.unpacked is a native node module. This assumption _may_ not be true so we should
|
||||||
pj = await fs.readJson(path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'));
|
* look at codifying that assumption as actual logic.
|
||||||
} else {
|
*/
|
||||||
pj = JSON.parse((await asar.extractFile(path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app.asar'), 'package.json')).toString('utf8'));
|
// FIXME: Codify the assumption that app.asar.unpacked only contains native modules
|
||||||
}
|
if (x64AsarMode === AsarMode.HAS_ASAR) {
|
||||||
pj.main = 'index.js';
|
d('checking if the x64 and arm64 asars are identical');
|
||||||
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
||||||
await asar.createPackage(entryAsar, path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
const arm64AsarSha = await sha(
|
||||||
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
);
|
||||||
|
|
||||||
for (const snapshotsFile of arm64Files.filter(f => f.type === AppFileType.SNAPSHOT)) {
|
if (x64AsarSha !== arm64AsarSha) {
|
||||||
await fs.copy(path.resolve(opts.arm64AppPath, snapshotsFile.relativePath), path.resolve(tmpApp, snapshotsFile.relativePath));
|
d('x64 and arm64 asars are different');
|
||||||
|
await fs.move(
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'),
|
||||||
|
);
|
||||||
|
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
|
||||||
|
if (await fs.pathExists(x64Unpacked)) {
|
||||||
|
await fs.move(
|
||||||
|
x64Unpacked,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.copy(
|
||||||
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'),
|
||||||
|
);
|
||||||
|
const arm64Unpacked = path.resolve(
|
||||||
|
opts.arm64AppPath,
|
||||||
|
'Contents',
|
||||||
|
'Resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(arm64Unpacked)) {
|
||||||
|
await fs.copy(
|
||||||
|
arm64Unpacked,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
||||||
|
await fs.mkdir(entryAsar);
|
||||||
|
await fs.copy(
|
||||||
|
path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'),
|
||||||
|
path.resolve(entryAsar, 'index.js'),
|
||||||
|
);
|
||||||
|
let pj = JSON.parse(
|
||||||
|
(
|
||||||
|
await asar.extractFile(
|
||||||
|
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
'package.json',
|
||||||
|
)
|
||||||
|
).toString('utf8'),
|
||||||
|
);
|
||||||
|
pj.main = 'index.js';
|
||||||
|
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||||
|
await asar.createPackage(
|
||||||
|
entryAsar,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
d('x64 and arm64 asars are the same');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) {
|
||||||
|
d('copying snapshot file', snapshotsFile.relativePath, 'to target application');
|
||||||
|
await fs.copy(
|
||||||
|
path.resolve(opts.arm64AppPath, snapshotsFile.relativePath),
|
||||||
|
path.resolve(tmpApp, snapshotsFile.relativePath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
d('moving final universal app to target destination');
|
||||||
|
await fs.mkdirp(path.dirname(opts.outAppPath));
|
||||||
await spawn('mv', [tmpApp, opts.outAppPath]);
|
await spawn('mv', [tmpApp, opts.outAppPath]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
16
src/sha.ts
Normal file
16
src/sha.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { d } from './debug';
|
||||||
|
|
||||||
|
export const sha = async (filePath: string) => {
|
||||||
|
d('hashing', filePath);
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.setEncoding('hex');
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
fileStream.pipe(hash);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
fileStream.on('end', () => resolve());
|
||||||
|
fileStream.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
|
return hash.read();
|
||||||
|
};
|
||||||
65
yarn.lock
65
yarn.lock
@@ -269,6 +269,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
||||||
|
|
||||||
|
"@types/debug@^4.1.5":
|
||||||
|
version "4.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
|
||||||
|
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
|
||||||
|
|
||||||
"@types/fs-extra@^9.0.4":
|
"@types/fs-extra@^9.0.4":
|
||||||
version "9.0.4"
|
version "9.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa"
|
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa"
|
||||||
@@ -616,6 +621,11 @@ braces@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
buffer-equal@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
|
||||||
|
integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74=
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
buffer-from@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||||
@@ -900,6 +910,13 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
commander@2.9.0:
|
||||||
|
version "2.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
|
||||||
|
integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=
|
||||||
|
dependencies:
|
||||||
|
graceful-readlink ">= 1.0.0"
|
||||||
|
|
||||||
commander@^5.0.0:
|
commander@^5.0.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
||||||
@@ -1131,6 +1148,13 @@ debug@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
|
debug@^4.3.1:
|
||||||
|
version "4.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||||
|
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||||
|
dependencies:
|
||||||
|
ms "2.1.2"
|
||||||
|
|
||||||
debuglog@^1.0.1:
|
debuglog@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||||
@@ -1211,6 +1235,16 @@ dezalgo@^1.0.0, dezalgo@~1.0.3:
|
|||||||
asap "^2.0.0"
|
asap "^2.0.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
|
dir-compare@^2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631"
|
||||||
|
integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==
|
||||||
|
dependencies:
|
||||||
|
buffer-equal "1.0.0"
|
||||||
|
colors "1.0.3"
|
||||||
|
commander "2.9.0"
|
||||||
|
minimatch "3.0.4"
|
||||||
|
|
||||||
dir-glob@^3.0.0, dir-glob@^3.0.1:
|
dir-glob@^3.0.0, dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||||
@@ -1804,6 +1838,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
|||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||||
|
|
||||||
|
"graceful-readlink@>= 1.0.0":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||||
|
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
|
||||||
|
|
||||||
handlebars@^4.7.6:
|
handlebars@^4.7.6:
|
||||||
version "4.7.6"
|
version "4.7.6"
|
||||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
|
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
|
||||||
@@ -2036,9 +2075,9 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1,
|
|||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
|
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
|
||||||
version "1.3.5"
|
version "1.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||||
|
|
||||||
init-package-json@^1.10.3:
|
init-package-json@^1.10.3:
|
||||||
version "1.10.3"
|
version "1.10.3"
|
||||||
@@ -2864,7 +2903,7 @@ min-indent@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||||
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
||||||
|
|
||||||
minimatch@^3.0.4:
|
minimatch@3.0.4, minimatch@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||||
@@ -4103,9 +4142,9 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
|||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
semantic-release@^17.2.2:
|
semantic-release@^17.2.2:
|
||||||
version "17.2.2"
|
version "17.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-17.2.2.tgz#520dae9cd188c7cdcc5216a7aad131548fc5cec7"
|
resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-17.2.3.tgz#11f10b851d4e75b1015b17515c433049b3df994c"
|
||||||
integrity sha512-LNU68ud3a3oU46H11OThXaKAK430jGGGTIF4VsiP843kRmS6s8kVCceLRdi7yWWz/sCCMD0zygPTQV2Jw79J5g==
|
integrity sha512-MY1MlowGQrkOR7+leOD8ICkVOC6i1szbwDODdbJ0UdshtMx8Ms0bhpRQmEEliqYKEb5PLv/dqs6zKKuHT7UxTg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@semantic-release/commit-analyzer" "^8.0.0"
|
"@semantic-release/commit-analyzer" "^8.0.0"
|
||||||
"@semantic-release/error" "^2.2.0"
|
"@semantic-release/error" "^2.2.0"
|
||||||
@@ -4365,9 +4404,9 @@ sshpk@^1.7.0:
|
|||||||
tweetnacl "~0.14.0"
|
tweetnacl "~0.14.0"
|
||||||
|
|
||||||
ssri@^6.0.0, ssri@^6.0.1:
|
ssri@^6.0.0, ssri@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
|
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
|
||||||
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
|
integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
figgy-pudding "^3.5.1"
|
figgy-pudding "^3.5.1"
|
||||||
|
|
||||||
@@ -5004,9 +5043,9 @@ xtend@~4.0.1:
|
|||||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||||
|
|
||||||
y18n@^3.2.1:
|
y18n@^3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
|
||||||
integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
|
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
|
||||||
|
|
||||||
y18n@^4.0.0:
|
y18n@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user