feat(youtube-player): initialize component and infrastructure (#16337)

This commit is contained in:
Nathan Tate
2019-07-31 16:44:50 -07:00
committed by Jeremy Elbourn
parent 3ec531b71a
commit b8c9da6262
26 changed files with 622 additions and 1 deletions
+3
View File
@@ -57,6 +57,9 @@
/src/material/core/typography/** @crisbeto
/src/material/core/util/** @jelbourn
# Miscellaneous components
/src/youtube-player/** @nathantate
# CDK
/src/cdk/* @jelbourn
/src/cdk/a11y/** @jelbourn @devversion
+1
View File
@@ -45,6 +45,7 @@
"@angular/elements": "^8.1.0",
"@angular/forms": "^8.1.0",
"@angular/platform-browser": "^8.1.0",
"@types/youtube": "^0.0.38",
"@webcomponents/custom-elements": "^1.1.0",
"core-js": "^2.6.1",
"material-components-web": "^3.0.0",
+1
View File
@@ -121,6 +121,7 @@ ROLLUP_GLOBALS = {
"@angular/cdk-experimental": "ng.cdkExperimental",
"@angular/material": "ng.material",
"@angular/material-experimental": "ng.materialExperimental",
"@angular/youtube-player": "ng.youtubePlayer",
}
# Rollup globals for cdk subpackages in the form of, e.g., {"@angular/cdk/table": "ng.cdk.table"}
+65
View File
@@ -0,0 +1,65 @@
package(default_visibility = ["//visibility:public"])
load("//:packages.bzl", "ROLLUP_GLOBALS")
load(
"//tools:defaults.bzl",
"markdown_to_html",
"ng_module",
"ng_package",
"ng_test_library",
"ng_web_test_suite",
)
ng_module(
name = "youtube-player",
srcs = glob(
["**/*.ts"],
exclude = [
"**/*.spec.ts",
"fake-youtube-player.ts",
],
),
module_name = "@angular/youtube-player",
deps = [
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@types/youtube",
"@npm//rxjs",
],
)
ng_package(
name = "npm_package",
srcs = ["package.json"],
entry_point = ":public-api.ts",
entry_point_name = "youtube-player",
globals = ROLLUP_GLOBALS,
deps = [":youtube-player"],
)
ng_test_library(
name = "unit_test_sources",
srcs = ["fake-youtube-player.ts"] + glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":youtube-player",
"@npm//@angular/platform-browser",
],
)
ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)
markdown_to_html(
name = "overview",
srcs = [":youtube-player.md"],
)
filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)
View File
+61
View File
@@ -0,0 +1,61 @@
// A re-creation of YT.PlayerState since enum values cannot be bound to the window
// object.
const playerState = {
UNSTARTED: -1,
ENDED: 0,
PLAYING: 1,
PAUSED: 2,
BUFFERING: 3,
CUED: 5,
};
interface FakeYtNamespace {
playerCtorSpy: jasmine.Spy;
playerSpy: jasmine.SpyObj<YT.Player>;
onPlayerReady: () => void;
namespace: typeof YT;
}
export function createFakeYtNamespace(): FakeYtNamespace {
const playerSpy: jasmine.SpyObj<YT.Player> = jasmine.createSpyObj('Player', [
'getPlayerState', 'destroy', 'cueVideoById', 'loadVideoById', 'pauseVideo', 'stopVideo',
'seekTo', 'isMuted', 'mute', 'unMute', 'getVolume', 'getPlaybackRate',
'getAvailablePlaybackRates', 'getVideoLoadedFraction', 'getPlayerState', 'getCurrentTime',
'getPlaybackQuality', 'getAvailableQualityLevels', 'getDuration', 'getVideoUrl',
'getVideoEmbedCode', 'playVideo', 'setSize', 'setVolume', 'setPlaybackQuality',
'setPlaybackRate', 'addEventListener', 'removeEventListener',
]);
let playerConfig: YT.PlayerOptions | undefined;
const playerCtorSpy = jasmine.createSpy('Player Constructor');
playerCtorSpy.and.callFake((_el: Element, config: YT.PlayerOptions) => {
playerConfig = config;
return playerSpy;
});
const onPlayerReady = () => {
if (!playerConfig) {
throw new Error('Player not initialized before onPlayerReady called');
}
if (playerConfig && playerConfig.events && playerConfig.events.onReady) {
playerConfig.events.onReady({target: playerSpy});
}
for (const [event, callback] of playerSpy.addEventListener.calls.allArgs()) {
if (event === 'onReady') {
callback({target: playerSpy});
}
}
};
return {
playerCtorSpy,
playerSpy,
onPlayerReady,
namespace: {
'Player': playerCtorSpy as unknown as typeof YT.Player,
'PlayerState': playerState,
} as typeof YT,
};
}
+1
View File
@@ -0,0 +1 @@
export * from './public-api';
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@angular/youtube-player",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular YouTube Player",
"main": "./bundles/youtube-player.umd.js",
"module": "./esm5/youtube-player.es5.js",
"es2015": "./esm2015/youtube-player.js",
"typings": "./youtube-player.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/angular/components.git"
},
"keywords": [
"angular",
"components",
"youtube"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/angular/components/issues"
},
"homepage": "https://github.com/angular/components/tree/master/src/youtube-player#readme",
"dependencies": {
"@types/youtube": "^0.0.38"
},
"peerDependencies": {
"@angular/core": "0.0.0-NG",
"@angular/common": "0.0.0-NG"
},
"sideEffects": false
}
+2
View File
@@ -0,0 +1,2 @@
export * from './youtube-module';
export * from './youtube-player';
+28
View File
@@ -0,0 +1,28 @@
// TypeScript config file that is used to compile the Material package through Gulp. As the
// long term goal is to switch to Bazel, and we already want to run tests with Bazel, we need to
// ensure the TypeScript build options are the same for Gulp and Bazel. We achieve this by
// extending the generic Bazel build tsconfig which will be used for each entry-point.
{
"extends": "../bazel-tsconfig-build.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "../../dist/packages/youtube-player",
"rootDir": ".",
"rootDirs": [
".",
"../../dist/packages/youtube-player"
],
"types": ["youtube"]
},
"files": [
"public-api.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/youtube-player",
"skipTemplateCodegen": true,
"fullTemplateTypeCheck": true
}
}
+26
View File
@@ -0,0 +1,26 @@
// TypeScript config file that extends the default tsconfig file for the library. This config is
// used to compile the tests for Karma. Since the code will run inside of the browser, the target
// needs to be ES5. The format needs to be CommonJS since Karma only supports that module format.
{
"extends": "./tsconfig-build",
"compilerOptions": {
"importHelpers": false,
"module": "commonjs",
"target": "es5",
"types": ["jasmine", "youtube"]
},
"angularCompilerOptions": {
"strictMetadataEmit": true,
"skipTemplateCodegen": true,
"emitDecoratorMetadata": true,
"fullTemplateTypeCheck": true,
// Unset options inherited from tsconfig-build
"annotateForClosureCompiler": false,
"flatModuleOutFile": null,
"flatModuleId": null
},
"include": [
"*.ts"
]
}
+11
View File
@@ -0,0 +1,11 @@
// Configuration for IDEs only.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"baseUrl": ".",
"paths": {},
"types": ["jasmine", "youtube"]
},
"include": ["*.ts"]
}
+14
View File
@@ -0,0 +1,14 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {YouTubePlayer} from './youtube-player';
const COMPONENTS = [YouTubePlayer];
@NgModule({
declarations: COMPONENTS,
exports: COMPONENTS,
imports: [CommonModule],
})
export class YouTubePlayerModule {
}
+101
View File
@@ -0,0 +1,101 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {YouTubePlayerModule} from './index';
import {YouTubePlayer} from './youtube-player';
import {createFakeYtNamespace} from './fake-youtube-player';
const VIDEO_ID = 'a12345';
declare global {
interface Window { YT: typeof YT | undefined; }
}
describe('YoutubePlayer', () => {
let playerCtorSpy: jasmine.Spy;
let playerSpy: jasmine.SpyObj<YT.Player>;
let onPlayerReady: () => void;
let fixture: ComponentFixture<TestApp>;
let testComponent: TestApp;
beforeEach(async(() => {
const fake = createFakeYtNamespace();
playerCtorSpy = fake.playerCtorSpy;
playerSpy = fake.playerSpy;
onPlayerReady = fake.onPlayerReady;
window.YT = fake.namespace;
TestBed.configureTestingModule({
imports: [YouTubePlayerModule],
declarations: [TestApp],
});
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestApp);
testComponent = fixture.debugElement.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
window.YT = undefined;
});
it('initializes a youtube player', () => {
let containerElement = fixture.nativeElement.querySelector('div');
expect(playerCtorSpy).toHaveBeenCalledWith(
containerElement, jasmine.objectContaining({
videoId: VIDEO_ID,
}));
});
it('destroys the iframe when the component is destroyed', () => {
onPlayerReady();
testComponent.visible = false;
fixture.detectChanges();
expect(playerSpy.destroy).toHaveBeenCalled();
});
it('responds to changes in video id', () => {
let containerElement = fixture.nativeElement.querySelector('div');
testComponent.videoId = 'otherId';
fixture.detectChanges();
expect(playerSpy.cueVideoById).not.toHaveBeenCalled();
onPlayerReady();
expect(playerSpy.cueVideoById).toHaveBeenCalledWith(
jasmine.objectContaining({videoId: 'otherId'}));
testComponent.videoId = undefined;
fixture.detectChanges();
expect(playerSpy.destroy).toHaveBeenCalled();
testComponent.videoId = 'otherId2';
fixture.detectChanges();
expect(playerCtorSpy).toHaveBeenCalledWith(
containerElement, jasmine.objectContaining({videoId: 'otherId2'}));
});
});
/** Test component that contains a YouTubePlayer. */
@Component({
selector: 'test-app',
template: `
<youtube-player #player [videoId]="videoId" *ngIf="visible">
</youtube-player>
`
})
class TestApp {
videoId: string | undefined = VIDEO_ID;
visible = true;
@ViewChild('player', {static: true}) youtubePlayer: YouTubePlayer;
}
+241
View File
@@ -0,0 +1,241 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
ViewChild,
} from '@angular/core';
import {
combineLatest,
of as observableOf,
Observable,
ConnectableObservable,
pipe,
MonoTypeOperatorFunction,
merge,
OperatorFunction,
} from 'rxjs';
import {
map,
scan,
withLatestFrom,
flatMap,
filter,
startWith,
publish,
first,
distinctUntilChanged,
takeUntil,
} from 'rxjs/operators';
declare global {
interface Window { YT: typeof YT | undefined; }
}
// The native YT.Player doesn't expose the set videoId, but we need it for
// convenience.
interface Player extends YT.Player {
videoId?: string | undefined;
}
// The player isn't fully initialized when it's constructed.
// The only field available is destroy and addEventListener.
type UninitializedPlayer = Pick<Player, 'videoId' | 'destroy' | 'addEventListener'>;
/**
* Angular component that renders a YouTube player via the YouTube player
* iframe API.
* @see https://developers.google.com/youtube/iframe_api_reference
*/
@Component({
selector: 'youtube-player',
changeDetection: ChangeDetectionStrategy.OnPush,
// This div is *replaced* by the YouTube player embed.
template: '<div #youtube_container></div>',
})
export class YouTubePlayer implements AfterViewInit, OnDestroy {
/** YouTube Video ID to view */
get videoId(): string | undefined {
return this._player && this._player.videoId;
}
@Input()
set videoId(videoId: string | undefined) {
this._videoId.emit(videoId);
}
private _videoId = new EventEmitter<string | undefined>();
/** The element that will be replaced by the iframe. */
@ViewChild('youtube_container', {static: false}) youtubeContainer: ElementRef | undefined;
private _youtubeContainer = new EventEmitter<HTMLElement>();
private _destroyed = new EventEmitter<undefined>();
private _player: Player | undefined;
constructor() {
if (!window.YT) {
throw new Error('Namespace YT not found, cannot construct embedded youtube player. ' +
'Please install the YouTube Player API Reference for iframe Embeds: ' +
'https://developers.google.com/youtube/iframe_api_reference');
}
/** An observable of the currently loaded player. */
const playerObs =
createPlayerObservable(
this._youtubeContainer,
this._videoId,
).pipe(waitUntilReady(), takeUntil(this._destroyed), publish());
/** Set up side effects to bind inputs to the player. */
playerObs.subscribe(player => this._player = player);
bindCueVideoCall(playerObs, this._videoId, this._destroyed);
// After all of the subscriptions are set up, connect the observable.
(playerObs as ConnectableObservable<Player>).connect();
}
ngAfterViewInit() {
if (!this.youtubeContainer) {
return;
}
this._youtubeContainer.emit(this.youtubeContainer.nativeElement);
}
ngOnDestroy() {
if (!this._player) {
return;
}
this._player.destroy();
this._destroyed.emit();
}
}
/**
* Returns an observable that emits the loaded player once it's ready. Certain properties/methods
* won't be available until the iframe finishes loading.
*/
function waitUntilReady()
: OperatorFunction<UninitializedPlayer | undefined, Player | undefined> {
return flatMap(player => {
if (!player) {
return observableOf<Player|undefined>(undefined);
}
if ('getPlayerStatus' in player) {
return observableOf(player as Player);
}
// The player is not initialized fully until the ready is called.
return fromPlayerOnReady(player)
.pipe(first(), startWith(undefined));
});
}
/** Since removeEventListener is not on Player when it's initialized, we can't use fromEvent. */
function fromPlayerOnReady(player: UninitializedPlayer): Observable<Player> {
return new Observable<Player>(emitter => {
let aborted = false;
const onReady = (event: YT.PlayerEvent) => {
if (aborted) {
return;
}
event.target.removeEventListener('onReady', onReady);
emitter.next(event.target);
};
player.addEventListener('onReady', onReady);
return () => {
aborted = true;
};
});
}
/** Create an observable for the player based on the given options. */
function createPlayerObservable(
youtubeContainer: Observable<HTMLElement>,
videoIdObs: Observable<string | undefined>,
): Observable<UninitializedPlayer | undefined> {
const playerOptions =
videoIdObs
.pipe(
map((videoId) => videoId ? {videoId} : undefined),
);
return combineLatest(youtubeContainer, playerOptions)
.pipe(scan(syncPlayerState, undefined), distinctUntilChanged());
}
/** Destroy the player if there are no options, or create the player if there are options. */
function syncPlayerState(
player: UninitializedPlayer | undefined,
[container, videoOptions]: [HTMLElement, YT.PlayerOptions | undefined],
): UninitializedPlayer | undefined {
if (!videoOptions) {
if (player) {
player.destroy();
}
return;
}
if (player) {
return player;
}
const newPlayer: UninitializedPlayer = new YT.Player(container, videoOptions);
// Bind videoId for future use.
newPlayer.videoId = videoOptions.videoId;
return newPlayer;
}
/**
* Call cueVideoById if the videoId changes, or when start or end seconds change. cueVideoById will
* change the loaded video id to the given videoId, and set the start and end times to the given
* start/end seconds.
*/
function bindCueVideoCall(
playerObs: Observable<Player | undefined>,
videoIdObs: Observable<string | undefined>,
destroyed: Observable<undefined>,
) {
// If the video id changed, there's no reason to run 'cue' unless the player
// was initialized with a different video id.
const changedVideoId = videoIdObs
.pipe(filterOnOther(playerObs, (player, videoId) => !!player && player.videoId !== videoId));
// If the player changed, there's no reason to run 'cue' unless there are cue options.
const changedPlayer = playerObs.pipe(
filterOnOther(videoIdObs, (videoId, player) => !!player && videoId != player.videoId));
merge(changedPlayer, changedVideoId)
.pipe(
withLatestFrom(combineLatest(playerObs, videoIdObs)),
map(([_, values]) => values),
takeUntil(destroyed),
)
.subscribe(([player, videoId]) => {
if (!videoId || !player) {
return;
}
player.videoId = videoId;
player.cueVideoById({videoId});
});
}
/** Combines the two observables temporarily for the filter function. */
function filterOnOther<R, T>(
otherObs: Observable<T>,
filterFn: (t: T, r?: R) => boolean,
): MonoTypeOperatorFunction<R> {
return pipe(
withLatestFrom(otherObs),
filter(([value, other]) => filterFn(other, value)),
map(([value]) => value),
);
}
+3 -1
View File
@@ -7,7 +7,8 @@ import {
examplesPackage,
materialExperimentalPackage,
materialPackage,
momentAdapterPackage
momentAdapterPackage,
youTubePlayerPackage
} from './packages';
createPackageBuildTasks(cdkPackage);
@@ -16,6 +17,7 @@ createPackageBuildTasks(materialPackage);
createPackageBuildTasks(materialExperimentalPackage);
createPackageBuildTasks(examplesPackage, ['build-examples-module']);
createPackageBuildTasks(momentAdapterPackage);
createPackageBuildTasks(youTubePlayerPackage);
import './tasks/aot';
import './tasks/breaking-changes';
+2
View File
@@ -2,6 +2,7 @@ import {BuildPackage} from 'material2-build-tools';
export const cdkPackage = new BuildPackage('cdk');
export const materialPackage = new BuildPackage('material', [cdkPackage]);
export const youTubePlayerPackage = new BuildPackage('youtube-player');
export const cdkExperimentalPackage = new BuildPackage('cdk-experimental', [cdkPackage]);
export const materialExperimentalPackage = new BuildPackage('material-experimental',
[cdkPackage, cdkExperimentalPackage, materialPackage]);
@@ -32,6 +33,7 @@ materialPackage.hasSchematics = true;
export const allBuildPackages = [
cdkPackage,
materialPackage,
youTubePlayerPackage,
cdkExperimentalPackage,
materialExperimentalPackage,
momentAdapterPackage,
+1
View File
@@ -32,6 +32,7 @@ task('build-aot:release-packages', sequenceTask(
[
'cdk:build-release',
'material:build-release',
'youtube-player:build-release',
'cdk-experimental:build-release',
'material-experimental:build-release',
'material-moment-adapter:build-release',
+8
View File
@@ -15,6 +15,7 @@ import {
materialPackage,
momentAdapterPackage,
examplesPackage,
youTubePlayerPackage,
} from '../packages';
import {watchFilesAndReload} from '../util/watch-files-reload';
@@ -64,6 +65,7 @@ task('build:devapp', sequenceTask(
'material:build-no-bundles',
'cdk-experimental:build-no-bundles',
'material-experimental:build-no-bundles',
'youtube-player:build-no-bundles',
'material-moment-adapter:build-no-bundles',
'build-examples-module',
// The examples module needs to be manually built before building examples package because
@@ -95,6 +97,8 @@ task('stage-deploy:devapp', ['build:devapp'], () => {
join(deployOutputDir, 'dist/packages/material'));
copyFiles(materialExperimentalPackage.outputDir, '**/*.+(js|map)',
join(deployOutputDir, 'dist/packages/material-experimental'));
copyFiles(youTubePlayerPackage.outputDir, '**/*.+(js|map)',
join(deployOutputDir, 'dist/packages/youtube-player'));
copyFiles(cdkExperimentalPackage.outputDir, '**/*.+(js|map)',
join(deployOutputDir, 'dist/packages/cdk-experimental'));
copyFiles(materialPackage.outputDir, '**/prebuilt/*.+(css|map)',
@@ -164,6 +168,10 @@ task(':watch:devapp', () => {
watchFilesAndReload(join(materialExperimentalPackage.sourceDir, '**/*'),
['material-experimental:build-no-bundles']);
// Youtube player package watchers
watchFilesAndReload(join(youTubePlayerPackage.sourceDir, '**/*'),
['youtube-player:build-no-bundles']);
// CDK experimental package watchers
watchFilesAndReload(join(cdkExperimentalPackage.sourceDir, '**/*'),
['cdk-experimental:build-no-bundles']);
+1
View File
@@ -19,6 +19,7 @@ task(':test:build', sequenceTask(
'material:build-no-bundles',
'cdk-experimental:build-no-bundles',
'material-experimental:build-no-bundles',
'youtube-player:build-no-bundles',
'material-moment-adapter:build-no-bundles'
));
+2
View File
@@ -41,6 +41,7 @@ task(
'material:build-release',
'cdk-experimental:build-release',
'material-experimental:build-release',
'youtube-player:build-release',
],
['universal:copy-release', 'universal:copy-files'],
['universal:build-app-ts', 'universal:build-app-scss'],
@@ -66,4 +67,5 @@ task('universal:copy-release', () => {
copySync(join(releasesDir, 'material'), join(outDir, 'material'));
copySync(join(releasesDir, 'cdk-experimental'), join(outDir, 'cdk-experimental'));
copySync(join(releasesDir, 'material-experimental'), join(outDir, 'material-experimental'));
copySync(join(releasesDir, 'youtube-player'), join(outDir, 'youtube-player'));
});
+1
View File
@@ -135,6 +135,7 @@ const packageDirs: string[] = [
'cdk',
'material',
'material-experimental',
'youtube-player',
'cdk-experimental',
];
+12
View File
@@ -30,6 +30,10 @@ const cdkExperimentalSecondaryEntryPoints =
const materialExperimentalSecondaryEntryPoints =
getSubdirectoryNames(join(buildConfig.packagesDir, 'material-experimental'));
/** List of potential secondary entry points for the youtube-player package. */
const youTubePlayerSecondaryEntryPoints =
getSubdirectoryNames(join(buildConfig.packagesDir, 'youtube-player'));
/** Object with all cdk entry points in the format of Rollup globals. */
const rollupCdkEntryPoints = generateRollupEntryPoints('cdk', cdkSecondaryEntryPoints);
@@ -40,6 +44,10 @@ const rollupMatEntryPoints = generateRollupEntryPoints('material', matSecondaryE
const rollupMaterialExperimentalEntryPoints =
generateRollupEntryPoints('material-experimental', materialExperimentalSecondaryEntryPoints);
/** Object with all youtube-player entry points in the format of Rollup globals. */
const rollupYouTubePlayerEntryPoints =
generateRollupEntryPoints('youtube-player', youTubePlayerSecondaryEntryPoints);
/** Object with all cdk-experimental entry points in the format of Rollup globals. */
const rollupCdkExperimentalEntryPoints =
generateRollupEntryPoints('cdk-experimental', cdkExperimentalSecondaryEntryPoints);
@@ -106,11 +114,15 @@ export const rollupGlobals = {
'@angular/material-experimental': 'ng.materialExperimental',
'@angular/material-moment-adapter': 'ng.materialMomentAdapter',
// Miscellaneous components
'@angular/youtube-player': 'ng.youtubePlayer',
// Include secondary entry-points of the cdk and material packages
...rollupCdkEntryPoints,
...rollupMatEntryPoints,
...rollupCdkExperimentalEntryPoints,
...rollupMaterialExperimentalEntryPoints,
...rollupYouTubePlayerEntryPoints,
'@angular/cdk/private/testing': 'ng.cdk.private.testing',
'@angular/cdk/private/testing/e2e': 'ng.cdk.private.testing.e2e',
@@ -2,6 +2,7 @@
export const releasePackages = [
'cdk',
'material',
'youtube-player',
'cdk-experimental',
'material-experimental',
'material-moment-adapter'
+5
View File
@@ -1521,6 +1521,11 @@
dependencies:
"@types/node" "*"
"@types/youtube@^0.0.38":
version "0.0.38"
resolved "https://registry.yarnpkg.com/@types/youtube/-/youtube-0.0.38.tgz#04aa5efa61f4d52e9cf682575a9dfed58f31e8c1"
integrity sha512-rS0nLlnnZIvOfBNT4957rkKRMHjHxutoVhSEzowocYgNiTMVBmN+Hbkf/L3TeGMMtts9uKZ9gw7O2JLHREVnIw==
"@types/z-schema@3.16.31":
version "3.16.31"
resolved "https://registry.yarnpkg.com/@types/z-schema/-/z-schema-3.16.31.tgz#2eb1d00a5e4ec3fa58c76afde12e182b66dc5c1c"