mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
8 Commits
videoframe
...
v42.0.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b3e1e8ef6 | ||
|
|
045516d598 | ||
|
|
1fffaeb481 | ||
|
|
88e666f210 | ||
|
|
90f7796adb | ||
|
|
e6925bef1f | ||
|
|
6a5e9fe677 | ||
|
|
270c9e7ce9 |
3
.github/workflows/audit-branch-ci.yml
vendored
3
.github/workflows/audit-branch-ci.yml
vendored
@@ -86,7 +86,6 @@ jobs:
|
||||
!message.startsWith("Response status code does not indicate success") &&
|
||||
!message.startsWith("The hosted runner lost communication with the server") &&
|
||||
!message.startsWith("Dependabot encountered an error performing the update") &&
|
||||
!message.startsWith("The action 'Run Electron Tests' has timed out") &&
|
||||
!/Unable to make request/.test(message) &&
|
||||
!/The requested URL returned error/.test(message),
|
||||
)
|
||||
@@ -155,7 +154,7 @@ jobs:
|
||||
await core.summary.write();
|
||||
- name: Send Slack message if errors
|
||||
if: ${{ always() && steps.audit-errors.outputs.errorsFound && github.ref == 'refs/heads/main' }}
|
||||
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
|
||||
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||
with:
|
||||
payload: |
|
||||
link: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
19
.github/workflows/branch-created.yml
vendored
19
.github/workflows/branch-created.yml
vendored
@@ -31,8 +31,8 @@ jobs:
|
||||
else
|
||||
echo "Not a release branch: $BRANCH_NAME"
|
||||
fi
|
||||
- name: Determine Next Unsupported Major Version
|
||||
id: determine-next-unsupported-major
|
||||
- name: Determine Unsupported Major Version
|
||||
id: determine-unsupported-major
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
env:
|
||||
MAJOR: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
@@ -50,27 +50,26 @@ jobs:
|
||||
|
||||
# Find the oldest version where eolDate >= stableDate of the new major
|
||||
# This gives us the oldest supported version when the new major goes stable
|
||||
NEXT_UNSUPPORTED_MAJOR=$(echo "$SCHEDULE" | jq -r --arg stableDate "$STABLE_DATE" '
|
||||
UNSUPPORTED_MAJOR=$(echo "$SCHEDULE" | jq -r --arg stableDate "$STABLE_DATE" '
|
||||
[.[] | select(.eolDate != null and .eolDate >= $stableDate)] | sort_by(.version | split(".")[0] | tonumber) | first | .version | split(".")[0]
|
||||
')
|
||||
|
||||
if [[ -z "$NEXT_UNSUPPORTED_MAJOR" || "$NEXT_UNSUPPORTED_MAJOR" == "null" ]]; then
|
||||
if [[ -z "$UNSUPPORTED_MAJOR" || "$UNSUPPORTED_MAJOR" == "null" ]]; then
|
||||
echo "Could not determine oldest supported version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SCHEDULE=$SCHEDULE" >> "$GITHUB_OUTPUT"
|
||||
echo "NEXT_UNSUPPORTED_MAJOR=$NEXT_UNSUPPORTED_MAJOR" >> "$GITHUB_OUTPUT"
|
||||
echo "UNSUPPORTED_MAJOR=$UNSUPPORTED_MAJOR" >> "$GITHUB_OUTPUT"
|
||||
- name: New Release Branch Tasks
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: electron/electron
|
||||
MAJOR: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
NEXT_UNSUPPORTED_MAJOR: ${{ steps.determine-next-unsupported-major.outputs.NEXT_UNSUPPORTED_MAJOR }}
|
||||
UNSUPPORTED_MAJOR: ${{ steps.determine-unsupported-major.outputs.UNSUPPORTED_MAJOR }}
|
||||
run: |
|
||||
PREVIOUS_MAJOR=$((MAJOR - 1))
|
||||
UNSUPPORTED_MAJOR=$((NEXT_UNSUPPORTED_MAJOR - 1))
|
||||
|
||||
# Create new labels
|
||||
gh label create $MAJOR-x-y --color 8d9ee8 || true
|
||||
@@ -109,8 +108,8 @@ jobs:
|
||||
id: generate-project-metadata
|
||||
env:
|
||||
MAJOR: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
NEXT_UNSUPPORTED_MAJOR: ${{ steps.determine-next-unsupported-major.outputs.NEXT_UNSUPPORTED_MAJOR }}
|
||||
SCHEDULE: ${{ steps.determine-next-unsupported-major.outputs.SCHEDULE }}
|
||||
UNSUPPORTED_MAJOR: ${{ steps.determine-unsupported-major.outputs.UNSUPPORTED_MAJOR }}
|
||||
SCHEDULE: ${{ steps.determine-unsupported-major.outputs.SCHEDULE }}
|
||||
with:
|
||||
script: |
|
||||
const schedule = JSON.parse(process.env.SCHEDULE)
|
||||
@@ -145,7 +144,7 @@ jobs:
|
||||
major,
|
||||
"next-major": nextMajor,
|
||||
"prev-major": prevMajor,
|
||||
"ending-support-major": parseInt(process.env.NEXT_UNSUPPORTED_MAJOR),
|
||||
"ending-support-major": parseInt(process.env.UNSUPPORTED_MAJOR),
|
||||
"beta-date": betaDate,
|
||||
"beta-prep-week": betaPrepWeek.toISOString().split('T')[0],
|
||||
"beta-prep-week-end": betaPrepWeekEnd.toISOString().split('T')[0],
|
||||
|
||||
37
.github/workflows/pr-triage-automation.yml
vendored
37
.github/workflows/pr-triage-automation.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: PR Triage Automation
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [synchronize, review_requested]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
set-needs-review:
|
||||
name: Set status to Needs Review
|
||||
if: >-
|
||||
(github.event_name == 'pull_request_target' && github.event.action == 'synchronize')
|
||||
|| (github.event_name == 'pull_request_target' && github.event.action == 'review_requested')
|
||||
|| (github.event_name == 'issue_comment'
|
||||
&& github.event.issue.pull_request
|
||||
&& github.event.comment.user.login == github.event.issue.user.login)
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
uses: electron/github-app-auth-action@e14e47722ed120360649d0789e25b9baece12725 # v2.0.0
|
||||
id: generate-token
|
||||
with:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status to Needs Review
|
||||
uses: dsanders11/project-actions/edit-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 118
|
||||
field: Status
|
||||
field-value: 🌀 Needs Review
|
||||
fail-if-item-not-found: false
|
||||
2
.github/workflows/pull-request-labeled.yml
vendored
2
.github/workflows/pull-request-labeled.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Trigger Slack workflow
|
||||
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
|
||||
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||
with:
|
||||
webhook: ${{ secrets.BACKPORT_REQUESTED_SLACK_WEBHOOK_URL }}
|
||||
webhook-type: webhook-trigger
|
||||
|
||||
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
@@ -51,6 +51,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -1585,20 +1585,6 @@ Centers the current text selection in web page.
|
||||
|
||||
Copy the image at the given position to the clipboard.
|
||||
|
||||
#### `contents.copyVideoFrameAt(x, y)`
|
||||
|
||||
* `x` Integer
|
||||
* `y` Integer
|
||||
|
||||
When executed on a video media element, copies the frame at (x, y) to the clipboard.
|
||||
|
||||
#### `contents.saveVideoFrameAs(x, y)`
|
||||
|
||||
* `x` Integer
|
||||
* `y` Integer
|
||||
|
||||
When executed on a video media element, shows a save dialog and saves the frame at (x, y) to disk.
|
||||
|
||||
#### `contents.paste()`
|
||||
|
||||
Executes the editing command `paste` in web page.
|
||||
|
||||
@@ -175,20 +175,6 @@ app.on('web-contents-created', (_, webContents) => {
|
||||
})
|
||||
```
|
||||
|
||||
#### `frame.copyVideoFrameAt(x, y)`
|
||||
|
||||
* `x` Integer
|
||||
* `y` Integer
|
||||
|
||||
When executed on a video media element, copies the frame at (x, y) to the clipboard.
|
||||
|
||||
#### `frame.saveVideoFrameAs(x, y)`
|
||||
|
||||
* `x` Integer
|
||||
* `y` Integer
|
||||
|
||||
When executed on a video media element, shows a save dialog and saves the frame at (x, y) to disk.
|
||||
|
||||
### Instance Properties
|
||||
|
||||
#### `frame.ipc` _Readonly_
|
||||
|
||||
@@ -179,7 +179,6 @@ auto_filenames = {
|
||||
"lib/common/define-properties.ts",
|
||||
"lib/common/deprecate.ts",
|
||||
"lib/common/ipc-messages.ts",
|
||||
"lib/common/timers-shim.ts",
|
||||
"lib/common/web-view-methods.ts",
|
||||
"lib/common/webpack-globals-provider.ts",
|
||||
"lib/renderer/api/context-bridge.ts",
|
||||
|
||||
@@ -437,14 +437,6 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
return p;
|
||||
};
|
||||
|
||||
WebContents.prototype.copyVideoFrameAt = function (x: number, y: number) {
|
||||
this.mainFrame.copyVideoFrameAt(x, y);
|
||||
};
|
||||
|
||||
WebContents.prototype.saveVideoFrameAs = function (x: number, y: number) {
|
||||
this.mainFrame.saveVideoFrameAs(x, y);
|
||||
};
|
||||
|
||||
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => Electron.WindowOpenHandlerResponse) {
|
||||
this._windowOpenHandler = handler;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"events": "^3.2.0",
|
||||
"folder-hash": "^4.1.2",
|
||||
"folder-hash": "^4.1.1",
|
||||
"got": "^11.8.5",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "shell/common/v8_util.h"
|
||||
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
|
||||
#include "third_party/blink/public/mojom/frame/media_player_action.mojom.h"
|
||||
#include "ui/gfx/geometry/point.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -262,28 +260,6 @@ v8::Local<v8::Promise> WebFrameMain::ExecuteJavaScript(
|
||||
return handle;
|
||||
}
|
||||
|
||||
void WebFrameMain::CopyVideoFrameAt(int x, int y) {
|
||||
if (!CheckRenderFrame())
|
||||
return;
|
||||
auto location = gfx::Point(x, y);
|
||||
auto action = blink::mojom::MediaPlayerAction(
|
||||
blink::mojom::MediaPlayerActionType::kCopyVideoFrame,
|
||||
/*enable=*/true);
|
||||
return render_frame_host()->ExecuteMediaPlayerActionAtLocation(location,
|
||||
action);
|
||||
}
|
||||
|
||||
void WebFrameMain::SaveVideoFrameAs(int x, int y) {
|
||||
if (!CheckRenderFrame())
|
||||
return;
|
||||
auto location = gfx::Point(x, y);
|
||||
auto action = blink::mojom::MediaPlayerAction(
|
||||
blink::mojom::MediaPlayerActionType::kSaveVideoFrameAs,
|
||||
/*enable=*/true);
|
||||
return render_frame_host()->ExecuteMediaPlayerActionAtLocation(location,
|
||||
action);
|
||||
}
|
||||
|
||||
bool WebFrameMain::Reload() {
|
||||
if (!CheckRenderFrame())
|
||||
return false;
|
||||
@@ -617,8 +593,6 @@ void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate,
|
||||
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
|
||||
.SetMethod("collectJavaScriptCallStack",
|
||||
&WebFrameMain::CollectDocumentJSCallStack)
|
||||
.SetMethod("copyVideoFrameAt", &WebFrameMain::CopyVideoFrameAt)
|
||||
.SetMethod("saveVideoFrameAs", &WebFrameMain::SaveVideoFrameAs)
|
||||
.SetMethod("reload", &WebFrameMain::Reload)
|
||||
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
|
||||
.SetMethod("_send", &WebFrameMain::Send)
|
||||
|
||||
@@ -118,8 +118,6 @@ class WebFrameMain final : public gin_helper::DeprecatedWrappable<WebFrameMain>,
|
||||
|
||||
v8::Local<v8::Promise> ExecuteJavaScript(gin::Arguments* args,
|
||||
const std::u16string& code);
|
||||
void CopyVideoFrameAt(int x, int y);
|
||||
void SaveVideoFrameAs(int x, int y);
|
||||
bool Reload();
|
||||
bool IsDestroyed() const;
|
||||
void Send(v8::Isolate* isolate,
|
||||
|
||||
@@ -1018,13 +1018,17 @@ void NativeWindowViews::MoveTop() {
|
||||
|
||||
bool NativeWindowViews::CanResize() const {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
return has_frame() ? resizable_ && thick_frame_ : resizable_;
|
||||
return resizable_ && thick_frame_;
|
||||
#else
|
||||
return resizable_;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NativeWindowViews::IsResizable() const {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (has_frame())
|
||||
return ::GetWindowLong(GetAcceleratedWidget(), GWL_STYLE) & WS_THICKFRAME;
|
||||
#endif
|
||||
return CanResize();
|
||||
}
|
||||
|
||||
|
||||
@@ -5611,7 +5611,7 @@ describe('BrowserWindow module', () => {
|
||||
thickFrame: true,
|
||||
transparent: true
|
||||
});
|
||||
expect(w.isResizable()).to.be.true('resizable');
|
||||
expect(w.isResizable()).to.be.false('resizable');
|
||||
w.maximize();
|
||||
expect(w.isMaximized()).to.be.true('maximized');
|
||||
const bounds = w.getBounds();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { clipboard } from 'electron/common';
|
||||
import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain, app, WebContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
@@ -535,91 +534,6 @@ describe('webFrameMain module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('webFrameMain.copyVideoFrameAt', () => {
|
||||
const insertVideoInFrame = async (frame: WebFrameMain) => {
|
||||
const videoFilePath = url.pathToFileURL(path.join(fixtures, 'cat-spin.mp4')).href;
|
||||
await frame.executeJavaScript(`
|
||||
const video = document.createElement('video');
|
||||
video.src = '${videoFilePath}';
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.play();
|
||||
document.body.appendChild(video);
|
||||
`);
|
||||
};
|
||||
|
||||
const getFramePosition = async (frame: WebFrameMain) => {
|
||||
const point = await frame.executeJavaScript(`(${() => {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (!iframe) return;
|
||||
const rect = iframe.getBoundingClientRect();
|
||||
return { x: Math.floor(rect.x), y: Math.floor(rect.y) };
|
||||
}})()`) as Electron.Point;
|
||||
expect(point).to.be.an('object');
|
||||
return point;
|
||||
};
|
||||
|
||||
const copyVideoFrameInFrame = async (frame: WebFrameMain) => {
|
||||
const point = await frame.executeJavaScript(`(${() => {
|
||||
const video = document.querySelector('video');
|
||||
if (!video) return;
|
||||
const rect = video.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.floor(rect.x + rect.width / 2),
|
||||
y: Math.floor(rect.y + rect.height / 2)
|
||||
};
|
||||
}})()`) as Electron.Point;
|
||||
|
||||
expect(point).to.be.an('object');
|
||||
|
||||
// Translate coordinate to be relative of main frame
|
||||
if (frame.parent) {
|
||||
const framePosition = await getFramePosition(frame.parent);
|
||||
point.x += framePosition.x;
|
||||
point.y += framePosition.y;
|
||||
}
|
||||
|
||||
expect(clipboard.readImage().isEmpty()).to.be.true();
|
||||
// wait for video to load
|
||||
await frame.executeJavaScript(`(${() => {
|
||||
const video = document.querySelector('video');
|
||||
if (!video) return;
|
||||
return new Promise(resolve => {
|
||||
if (video.readyState >= 4) resolve(null);
|
||||
else video.addEventListener('canplaythrough', resolve, { once: true });
|
||||
});
|
||||
}})()`);
|
||||
frame.copyVideoFrameAt(point.x, point.y);
|
||||
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
|
||||
expect(clipboard.readImage().isEmpty()).to.be.false();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
clipboard.clear();
|
||||
});
|
||||
|
||||
// TODO: Re-enable on Windows CI once Chromium fixes the intermittent
|
||||
// backwards-time DCHECK hit while copying video frames:
|
||||
// DCHECK failed: !delta.is_negative().
|
||||
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in main frame', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.webContents.loadFile(path.join(fixtures, 'blank.html'));
|
||||
await insertVideoInFrame(w.webContents.mainFrame);
|
||||
await copyVideoFrameInFrame(w.webContents.mainFrame);
|
||||
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
|
||||
});
|
||||
|
||||
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in subframe', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
|
||||
const subframe = w.webContents.mainFrame.frames[0];
|
||||
expect(subframe).to.exist();
|
||||
await insertVideoInFrame(subframe);
|
||||
await copyVideoFrameInFrame(subframe);
|
||||
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('"frame-created" event', () => {
|
||||
it('emits when the main frame is created', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -588,7 +588,7 @@ __metadata:
|
||||
eslint-plugin-node: "npm:^11.1.0"
|
||||
eslint-plugin-promise: "npm:^6.6.0"
|
||||
events: "npm:^3.2.0"
|
||||
folder-hash: "npm:^4.1.2"
|
||||
folder-hash: "npm:^4.1.1"
|
||||
got: "npm:^11.8.5"
|
||||
husky: "npm:^9.1.7"
|
||||
lint-staged: "npm:^16.1.0"
|
||||
@@ -3416,7 +3416,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^2.0.1, brace-expansion@npm:^2.0.2":
|
||||
"brace-expansion@npm:^2.0.1":
|
||||
version: 2.0.2
|
||||
resolution: "brace-expansion@npm:2.0.2"
|
||||
dependencies:
|
||||
@@ -6180,15 +6180,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"folder-hash@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "folder-hash@npm:4.1.2"
|
||||
"folder-hash@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "folder-hash@npm:4.1.1"
|
||||
dependencies:
|
||||
debug: "npm:4.4.0"
|
||||
minimatch: "npm:7.4.9"
|
||||
minimatch: "npm:7.4.6"
|
||||
bin:
|
||||
folder-hash: bin/folder-hash
|
||||
checksum: 10c0/7c26f7322820cff61745e168ed7c0d3fe9f9afafe7157d01de5cb708effc66761c2f4d1eda59d2925661baaac2adb8a04a51d0d1f01f8003d7e275610ca3f452
|
||||
checksum: 10c0/71597548cccda43c3d4bda940fd1277f63839a86322d66dec2aa883dce4f51c4c0a6e274d7cb30cfbf4df9897d7a5649a09257e5ffada2fa50cd3a2b09da5a32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9256,12 +9256,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:7.4.9":
|
||||
version: 7.4.9
|
||||
resolution: "minimatch@npm:7.4.9"
|
||||
"minimatch@npm:7.4.6":
|
||||
version: 7.4.6
|
||||
resolution: "minimatch@npm:7.4.6"
|
||||
dependencies:
|
||||
brace-expansion: "npm:^2.0.2"
|
||||
checksum: 10c0/8d5406a9697edb9b7ea02697d58cabcb3d3a9a4a02caa1cf57b9ab5ae22c78b2945600661a78f91d1545f77521f97f3cb5f8cb066e58356a121b50e4e60ccdbe
|
||||
brace-expansion: "npm:^2.0.1"
|
||||
checksum: 10c0/e587bf3d90542555a3d58aca94c549b72d58b0a66545dd00eef808d0d66e5d9a163d3084da7f874e83ca8cc47e91c670e6c6f6593a3e7bb27fcc0e6512e87c67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user