diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7a75033655ed..b3d36e29d263 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ # Date adapters /src/material-moment-adapter/** @mmalerba /src/material-luxon-adapter/** @crisbeto +/src/material-date-fns-adapter/** @crisbeto # Material experimental package /src/material-experimental/* @jelbourn diff --git a/.ng-dev/commit-message.ts b/.ng-dev/commit-message.ts index dfd48b1b157f..217a2ed7d65d 100644 --- a/.ng-dev/commit-message.ts +++ b/.ng-dev/commit-message.ts @@ -72,6 +72,7 @@ export const commitMessage: CommitMessageConfig = { 'material-experimental/popover-edit', 'material-experimental/selection', 'material-moment-adapter', + 'material-date-fns-adapter', 'material-luxon-adapter', 'material/autocomplete', 'material/badge', diff --git a/WORKSPACE b/WORKSPACE index 271aa4f11945..14ef704807c8 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -96,3 +96,7 @@ load( ) _dev_infra_browser_repositories() + +load("@npm//@bazel/esbuild:esbuild_repositories.bzl", "esbuild_repositories") + +esbuild_repositories() diff --git a/angular-tsconfig.json b/angular-tsconfig.json index db36ab2ad935..747f669f7eb7 100644 --- a/angular-tsconfig.json +++ b/angular-tsconfig.json @@ -38,6 +38,7 @@ "node_modules/@angular/material-experimental/**", "node_modules/@angular/material-moment-adapter/**", "node_modules/@angular/material-luxon-adapter/**", + "node_modules/@angular/material-date-fns-adapter/**", "node_modules/@angular/youtube-player/**" ] } diff --git a/package.json b/package.json index 11b10f5c2ae7..45f0b6890af5 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@bazel/bazelisk": "1.9.0", "@bazel/buildifier": "4.0.1", "@bazel/concatjs": "4.0.0-beta.0", + "@bazel/esbuild": "4.0.0-beta.0", "@bazel/ibazel": "0.15.10", "@bazel/jasmine": "4.0.0-beta.0", "@bazel/protractor": "4.0.0-beta.0", @@ -171,6 +172,7 @@ "chalk": "^4.1.0", "codelyzer": "^6.0.2", "conventional-changelog": "^3.0.5", + "date-fns": "^2.23.0", "dgeni": "^0.4.11", "dgeni-packages": "^0.28.4", "diff": "^5.0.0", @@ -195,13 +197,13 @@ "karma-requirejs": "^1.1.0", "karma-sauce-launcher": "^4.3.6", "karma-sourcemap-loader": "^0.3.7", + "luxon": "^2.0.0", "madge": "^4.0.0", "marked": "^2.0.0", "merge2": "^1.2.3", "minimatch": "^3.0.4", "minimist": "^1.2.0", "moment": "^2.18.1", - "luxon": "^2.0.0", "node-fetch": "^2.6.0", "parse5": "^6.0.1", "postcss": "^8.2.1", diff --git a/rollup-globals.bzl b/rollup-globals.bzl index 5dfbeb36dcf1..6dd4b8ddc00a 100644 --- a/rollup-globals.bzl +++ b/rollup-globals.bzl @@ -36,6 +36,7 @@ ROLLUP_GLOBALS = { "@angular/material-experimental": "ng.materialExperimental", "@angular/material-moment-adapter": "ng.materialMomentAdapter", "@angular/material-luxon-adapter": "ng.materialLuxonAdapter", + "@angular/material-date-fns-adapter": "ng.materialDateFnsAdapter", "@angular/youtube-player": "ng.youtubePlayer", # This UMD module name would not match with anything that MDC provides, but we just @@ -48,6 +49,7 @@ ROLLUP_GLOBALS = { "moment/locale/fr": "moment.locale.fr", "moment/locale/ja": "moment.locale.ja", "luxon": "luxon", + "date-fns": "dateFns", "protractor": "protractor", "rxjs": "rxjs", "rxjs/operators": "rxjs.operators", diff --git a/scripts/deploy/publish-build-artifacts.sh b/scripts/deploy/publish-build-artifacts.sh index a7ae78522726..87d373062de9 100755 --- a/scripts/deploy/publish-build-artifacts.sh +++ b/scripts/deploy/publish-build-artifacts.sh @@ -23,6 +23,7 @@ PACKAGES=( material-experimental material-moment-adapter # material-luxon-adapter TODO(crisbeto): enable this once we have a builds repo + # material-date-fns-adapter TODO(crisbeto): enable this once we have a builds repo google-maps youtube-player ) diff --git a/src/components-examples/package.json b/src/components-examples/package.json index 3f911c36eec5..49244d316c64 100644 --- a/src/components-examples/package.json +++ b/src/components-examples/package.json @@ -26,7 +26,8 @@ "@angular/material": "0.0.0-PLACEHOLDER", "@angular/material-experimental": "0.0.0-PLACEHOLDER", "@angular/material-moment-adapter": "0.0.0-PLACEHOLDER", - "@angular/material-luxon-adapter": "0.0.0-PLACEHOLDER" + "@angular/material-luxon-adapter": "0.0.0-PLACEHOLDER", + "@angular/material-date-fns-adapter": "0.0.0-PLACEHOLDER" }, "dependencies": { "tslib": "0.0.0-TSLIB" diff --git a/src/components-examples/tsconfig.json b/src/components-examples/tsconfig.json index c8c393e3be79..f648d03d233d 100644 --- a/src/components-examples/tsconfig.json +++ b/src/components-examples/tsconfig.json @@ -12,7 +12,8 @@ "@angular/material/*": ["../material/*"], "@angular/material-experimental/*": ["../material-experimental/*"], "@angular/material-moment-adapter": ["../material-moment-adapter/public-api.ts"], - "@angular/material-luxon-adapter": ["../material-luxon-adapter/public-api.ts"] + "@angular/material-luxon-adapter": ["../material-luxon-adapter/public-api.ts"], + "@angular/material-date-fns-adapter": ["../material-date-fns-adapter/public-api.ts"] } }, "include": ["./**/*.ts"] diff --git a/src/dev-app/tsconfig.json b/src/dev-app/tsconfig.json index 37a5b959abbc..3af9d2cb05c7 100644 --- a/src/dev-app/tsconfig.json +++ b/src/dev-app/tsconfig.json @@ -15,6 +15,7 @@ "@angular/cdk-experimental": ["../cdk-experimental"], "@angular/material-moment-adapter": ["../material-moment-adapter/public-api.ts"], "@angular/material-luxon-adapter": ["../material-luxon-adapter/public-api.ts"], + "@angular/material-date-fns-adapter": ["../material-date-fns-adapter/public-api.ts"], "@angular/google-maps": ["../google-maps"], "@angular/components-examples": ["../components-examples"], "@angular/components-examples/*": ["../components-examples/*"], diff --git a/src/e2e-app/tsconfig.json b/src/e2e-app/tsconfig.json index f8ab016c2312..4f0a5cb5f393 100644 --- a/src/e2e-app/tsconfig.json +++ b/src/e2e-app/tsconfig.json @@ -13,6 +13,7 @@ "@angular/cdk-experimental": ["../cdk-experimental/"], "@angular/material-moment-adapter": ["../material-moment-adapter/"], "@angular/material-luxon-adapter": ["../material-luxon-adapter/"], + "@angular/material-date-fns-adapter": ["../material-date-fns-adapter/"], "@angular/components-examples": ["../components-examples/"], "@angular/components-examples/*": ["../components-examples/*"] } diff --git a/src/material-date-fns-adapter/BUILD.bazel b/src/material-date-fns-adapter/BUILD.bazel new file mode 100644 index 000000000000..1444f99b3770 --- /dev/null +++ b/src/material-date-fns-adapter/BUILD.bazel @@ -0,0 +1,64 @@ +load("//tools:defaults.bzl", "ng_module", "ng_package", "ng_test_library", "ng_web_test_suite") +load(":esbuild-amd.bzl", "esbuild_amd") + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "material-date-fns-adapter", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//src:dev_mode_types", + "//src/material/core", + "@npm//@angular/core", + "@npm//date-fns", + ], +) + +ng_test_library( + name = "unit_test_sources", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":material-date-fns-adapter", + "//src/material/core", + "@npm//date-fns", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [ + ":amd_date_fns", + ":amd_date_fns_locales", + ":unit_test_sources", + ], +) + +esbuild_amd( + name = "amd_date_fns", + testonly = True, + entry_point = "@npm//:node_modules/date-fns/esm/index.js", + module_name = "date-fns", + deps = ["@npm//date-fns"], +) + +esbuild_amd( + name = "amd_date_fns_locales", + testonly = True, + entry_point = "@npm//:node_modules/date-fns/esm/locale/index.js", + module_name = "date-fns/locale", + deps = ["@npm//date-fns"], +) + +ng_package( + name = "npm_package", + srcs = ["package.json"], + entry_point = ":public-api.ts", + tags = ["release-package"], + deps = [":material-date-fns-adapter"], +) diff --git a/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts b/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts new file mode 100644 index 000000000000..7d0337a9d063 --- /dev/null +++ b/src/material-date-fns-adapter/adapter/date-fns-adapter.spec.ts @@ -0,0 +1,329 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; +import {Locale} from 'date-fns'; +import {ja, enUS, da} from 'date-fns/locale'; +import {DateFnsModule} from './index'; + +const JAN = 0, FEB = 1, MAR = 2, DEC = 11; + +describe('DateFnsAdapter', () => { + let adapter: DateAdapter; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [DateFnsModule] + }).compileComponents(); + + adapter = TestBed.inject(DateAdapter); + adapter.setLocale(enUS); + })); + + it('should get year', () => { + expect(adapter.getYear(new Date(2017, JAN, 1))).toBe(2017); + }); + + it('should get month', () => { + expect(adapter.getMonth(new Date(2017, JAN, 1))).toBe(0); + }); + + it('should get date', () => { + expect(adapter.getDate(new Date(2017, JAN, 1))).toBe(1); + }); + + it('should get day of week', () => { + expect(adapter.getDayOfWeek(new Date(2017, JAN, 1))).toBe(0); + }); + + it('should get long month names', () => { + expect(adapter.getMonthNames('long')).toEqual([ + 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', + 'October', 'November', 'December' + ]); + }); + + it('should get short month names', () => { + expect(adapter.getMonthNames('short')).toEqual([ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]); + }); + + it('should get narrow month names', () => { + expect(adapter.getMonthNames('narrow')).toEqual([ + 'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D' + ]); + }); + + it('should get month names in a different locale', () => { + adapter.setLocale(da); + + expect(adapter.getMonthNames('long')).toEqual([ + 'januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', + 'august', 'september', 'oktober', 'november', 'december' + ]); + }); + + it('should get date names', () => { + expect(adapter.getDateNames()).toEqual([ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', + '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31' + ]); + }); + + it('should get date names in a different locale', () => { + adapter.setLocale(ja); + if (typeof Intl !== 'undefined') { + expect(adapter.getDateNames()).toEqual([ + '1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日', '11日', '12日', + '13日', '14日', '15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日', + '25日', '26日', '27日', '28日', '29日', '30日', '31日' + ]); + } else { + expect(adapter.getDateNames()).toEqual([ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', + '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31' + ]); + } + }); + + it('should get long day of week names', () => { + expect(adapter.getDayOfWeekNames('long')).toEqual([ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' + ]); + }); + + it('should get short day of week names', () => { + expect(adapter.getDayOfWeekNames('short')).toEqual([ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' + ]); + }); + + it('should get narrow day of week names', () => { + expect(adapter.getDayOfWeekNames('narrow')).toEqual([ + 'S', 'M', 'T', 'W', 'T', 'F', 'S' + ]); + }); + + it('should get day of week names in a different locale', () => { + adapter.setLocale(ja); + + expect(adapter.getDayOfWeekNames('long')).toEqual([ + '日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日' + ]); + }); + + it('should get year name', () => { + expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017'); + }); + + it('should get year name in a different locale', () => { + adapter.setLocale(ja); + expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017'); + }); + + it('should get first day of week', () => { + expect(adapter.getFirstDayOfWeek()).toBe(0); + }); + + it('should not create Date with month over/under-flow', () => { + expect(() => adapter.createDate(2017, 12, 1)).toThrow(); + expect(() => adapter.createDate(2017, -1, 1)).toThrow(); + }); + + it('should not create Date with date over/under-flow', () => { + expect(() => adapter.createDate(2017, JAN, 32)).toThrow(); + expect(() => adapter.createDate(2017, JAN, 0)).toThrow(); + }); + + it("should get today's date", () => { + expect(adapter.sameDate(adapter.today(), new Date())) + .toBe(true, "should be equal to today's date"); + }); + + it('should parse string according to given format', () => { + expect(adapter.parse('1/2/2017', 'M/d/yyyy')).toEqual(new Date(2017, JAN, 2)); + expect(adapter.parse('1/2/2017', 'd/M/yyyy')).toEqual(new Date(2017, FEB, 1)); + }); + + it('should parse string according to first matching format', () => { + expect(adapter.parse('1/2/2017', ['M/d/yyyy', 'yyyy/d/M'])).toEqual(new Date(2017, JAN, 2)); + expect(adapter.parse('1/2/2017', ['yyyy/d/M', 'M/d/yyyy'])).toEqual(new Date(2017, JAN, 2)); + }); + + it('should throw if parse formats are an empty array', () => { + expect(() => adapter.parse('1/2/2017', [])).toThrowError('Formats array must not be empty.'); + }); + + it('should parse number', () => { + const timestamp = new Date().getTime(); + expect(adapter.parse(timestamp, 'MM/dd/yyyy')!.getTime()).toEqual(timestamp); + }); + + it('should parse Date', () => { + let date = new Date(2017, JAN, 1); + expect(adapter.parse(date, 'MM/dd/yyyy')).toEqual(date); + }); + + it('should parse empty string as null', () => { + expect(adapter.parse('', 'MM/dd/yyyy')).toBeNull(); + }); + + it('should parse invalid value as invalid', () => { + let d = adapter.parse('hello', 'MM/dd/yyyy'); + expect(d).not.toBeNull(); + expect(adapter.isDateInstance(d)).toBe(true); + expect(adapter.isValid(d as Date)) + .toBe(false, 'Expected to parse as "invalid date" object'); + }); + + it('should format date according to given format', () => { + expect(adapter.format(new Date(2017, JAN, 2), 'MM/dd/yyyy')).toEqual('01/02/2017'); + }); + + it('should format with a different locale', () => { + let date = adapter.format(new Date(2017, JAN, 2), 'PP'); + + expect(stripDirectionalityCharacters(date)).toEqual('Jan 2, 2017'); + adapter.setLocale(da); + + date = adapter.format(new Date(2017, JAN, 2), 'PP'); + expect(stripDirectionalityCharacters(date)).toEqual('2. jan. 2017'); + }); + + it('should throw when attempting to format invalid date', () => { + expect(() => adapter.format(new Date(NaN), 'MM/dd/yyyy')) + .toThrowError(/DateFnsAdapter: Cannot format invalid date\./); + }); + + it('should add years', () => { + expect(adapter.addCalendarYears(new Date(2017, JAN, 1), 1)).toEqual(new Date(2018, JAN, 1)); + expect(adapter.addCalendarYears(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, JAN, 1)); + }); + + it('should respect leap years when adding years', () => { + expect(adapter.addCalendarYears(new Date(2016, FEB, 29), 1)).toEqual(new Date(2017, FEB, 28)); + expect(adapter.addCalendarYears(new Date(2016, FEB, 29), -1)).toEqual(new Date(2015, FEB, 28)); + }); + + it('should add months', () => { + expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, FEB, 1)); + expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 1)); + }); + + it('should respect month length differences when adding months', () => { + expect(adapter.addCalendarMonths(new Date(2017, JAN, 31), 1)).toEqual(new Date(2017, FEB, 28)); + expect(adapter.addCalendarMonths(new Date(2017, MAR, 31), -1)).toEqual(new Date(2017, FEB, 28)); + }); + + it('should add days', () => { + expect(adapter.addCalendarDays(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, JAN, 2)); + expect(adapter.addCalendarDays(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 31)); + }); + + it('should clone', () => { + let date = new Date(2017, JAN, 1); + let clone = adapter.clone(date); + + expect(clone).not.toBe(date); + expect(clone.getTime()).toEqual(date.getTime()); + }); + + it('should compare dates', () => { + expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 2))).toBeLessThan(0); + expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, FEB, 1))).toBeLessThan(0); + expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2018, JAN, 1))).toBeLessThan(0); + expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 1))).toBe(0); + expect(adapter.compareDate(new Date(2018, JAN, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0); + expect(adapter.compareDate(new Date(2017, FEB, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0); + expect(adapter.compareDate(new Date(2017, JAN, 2), new Date(2017, JAN, 1))).toBeGreaterThan(0); + }); + + it('should clamp date at lower bound', () => { + expect(adapter.clampDate( + new Date(2017, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1))) + .toEqual(new Date(2018, JAN, 1)); + }); + + it('should clamp date at upper bound', () => { + expect(adapter.clampDate( + new Date(2020, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1))) + .toEqual(new Date(2019, JAN, 1)); + }); + + it('should clamp date already within bounds', () => { + expect(adapter.clampDate( + new Date(2018, FEB, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1))) + .toEqual(new Date(2018, FEB, 1)); + }); + + it('should count today as a valid date instance', () => { + let d = new Date(); + expect(adapter.isValid(d)).toBe(true); + expect(adapter.isDateInstance(d)).toBe(true); + }); + + it('should count an invalid date as an invalid date instance', () => { + let d = new Date(NaN); + expect(adapter.isValid(d)).toBe(false); + expect(adapter.isDateInstance(d)).toBe(true); + }); + + it('should count a string as not a date instance', () => { + let d = '1/1/2017'; + expect(adapter.isDateInstance(d)).toBe(false); + }); + + it('should create valid dates from valid ISO strings', () => { + assertValidDate(adapter, adapter.deserialize('1985-04-12T23:20:50.52Z'), true); + assertValidDate(adapter, adapter.deserialize('1996-12-19T16:39:57-08:00'), true); + assertValidDate(adapter, adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true); + assertValidDate(adapter, adapter.deserialize('1990-13-31T23:59:00Z'), false); + assertValidDate(adapter, adapter.deserialize('1/1/2017'), false); + expect(adapter.deserialize('')).toBeNull(); + expect(adapter.deserialize(null)).toBeNull(); + assertValidDate(adapter, adapter.deserialize(new Date()), true); + assertValidDate(adapter, adapter.deserialize(new Date(NaN)), false); + assertValidDate(adapter, adapter.deserialize(new Date()), true); + assertValidDate(adapter, adapter.deserialize(new Date('Not valid')), false); + }); + + it('should create invalid date', () => { + assertValidDate(adapter, adapter.invalid(), false); + }); +}); + +describe('DateFnsAdapter with MAT_DATE_LOCALE override', () => { + let adapter: DateAdapter; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [DateFnsModule], + providers: [{provide: MAT_DATE_LOCALE, useValue: da}] + }).compileComponents(); + + adapter = TestBed.inject(DateAdapter); + })); + + it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => { + const date = adapter.format(new Date(2017, JAN, 2), 'PP'); + expect(stripDirectionalityCharacters(date)).toEqual('2. jan. 2017'); + }); +}); + +function stripDirectionalityCharacters(str: string) { + return str.replace(/[\u200e\u200f]/g, ''); +} + +function assertValidDate(adapter: DateAdapter, d: Date | null, valid: boolean) { + expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`); + expect(adapter.isValid(d!)).toBe(valid, + `Expected ${d} to be ${valid ? 'valid' : 'invalid'},` + + ` but was ${valid ? 'invalid' : 'valid'}`); +} diff --git a/src/material-date-fns-adapter/adapter/date-fns-adapter.ts b/src/material-date-fns-adapter/adapter/date-fns-adapter.ts new file mode 100644 index 000000000000..2b0b0158c0ed --- /dev/null +++ b/src/material-date-fns-adapter/adapter/date-fns-adapter.ts @@ -0,0 +1,238 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, Optional} from '@angular/core'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; +import { + Locale, + getMonth, + getYear, + getDate, + getDay, + getDaysInMonth, + formatISO, + addYears, + addMonths, + addDays, + isValid, + isDate, + format, + parseISO, + parse, +} from 'date-fns'; + +/** Creates an array and fills it with values. */ +function range(length: number, valueFunction: (index: number) => T): T[] { + const valuesArray = Array(length); + for (let i = 0; i < length; i++) { + valuesArray[i] = valueFunction(i); + } + return valuesArray; +} + +// date-fns doesn't have a way to read/print month names or days of the week directly, +// so we get them by formatting a date with a format that produces the desired month/day. +const MONTH_FORMATS = { + long: 'LLLL', + short: 'LLL', + narrow: 'LLLLL' +}; + +const DAY_OF_WEEK_FORMATS = { + long: 'EEEE', + short: 'EEE', + narrow: 'EEEEE' +}; + +/** Adds date-fns support to Angular Material. */ +@Injectable() +export class DateFnsAdapter extends DateAdapter { + constructor(@Optional() @Inject(MAT_DATE_LOCALE) matDateLocale: {}) { + super(); + super.setLocale(matDateLocale); + } + + getYear(date: Date): number { + return getYear(date); + } + + getMonth(date: Date): number { + return getMonth(date); + } + + getDate(date: Date): number { + return getDate(date); + } + + getDayOfWeek(date: Date): number { + return getDay(date); + } + + getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { + const pattern = MONTH_FORMATS[style]; + return range(12, i => this.format(new Date(2017, i, 1), pattern)); + } + + getDateNames(): string[] { + const dtf = typeof Intl !== 'undefined' ? new Intl.DateTimeFormat(this.locale.code, { + day: 'numeric', + timeZone: 'utc' + }) : null; + + return range(31, i => { + if (dtf) { + // date-fns doesn't appear to support this functionality. + // Fall back to `Intl` on supported browsers. + const date = new Date(); + date.setUTCFullYear(2017, 0, i + 1); + date.setUTCHours(0, 0, 0, 0); + return dtf.format(date).replace(/[\u200e\u200f]/g, ''); + } + + return i + ''; + }); + } + + getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { + const pattern = DAY_OF_WEEK_FORMATS[style]; + return range(7, i => this.format(new Date(2017, 0, i + 1), pattern)); + } + + getYearName(date: Date): string { + return this.format(date, 'y'); + } + + getFirstDayOfWeek(): number { + return this.locale.options?.weekStartsOn ?? 0; + } + + getNumDaysInMonth(date: Date): number { + return getDaysInMonth(date); + } + + clone(date: Date): Date { + return new Date(date.getTime()); + } + + createDate(year: number, month: number, date: number): Date { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + // Check for invalid month and date (except upper bound on date which we have to check after + // creating the Date). + if (month < 0 || month > 11) { + throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); + } + + if (date < 1) { + throw Error(`Invalid date "${date}". Date has to be greater than 0.`); + } + } + + // Passing the year to the constructor causes year numbers <100 to be converted to 19xx. + // To work around this we use `setFullYear` and `setHours` instead. + const result = new Date(); + result.setFullYear(year, month, date); + result.setHours(0, 0, 0, 0); + + // Check that the date wasn't above the upper bound for the month, causing the month to overflow + if (result.getMonth() != month && (typeof ngDevMode === 'undefined' || ngDevMode)) { + throw Error(`Invalid date "${date}" for month with index "${month}".`); + } + + return result; + } + + today(): Date { + return new Date(); + } + + parse(value: any, parseFormat: string | string[]): Date | null { + if (typeof value == 'string' && value.length > 0) { + const iso8601Date = parseISO(value); + + if (this.isValid(iso8601Date)) { + return iso8601Date; + } + + const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat]; + + if (!parseFormat.length) { + throw Error('Formats array must not be empty.'); + } + + for (const currentFormat of formats) { + const fromFormat = parse(value, currentFormat, new Date()); + + if (this.isValid(fromFormat)) { + return fromFormat; + } + } + + return this.invalid(); + } else if (typeof value === 'number') { + return new Date(value); + } else if (value instanceof Date) { + return this.clone(value); + } + + return null; + } + + format(date: Date, displayFormat: string): string { + if (!this.isValid(date)) { + throw Error('DateFnsAdapter: Cannot format invalid date.'); + } + + return format(date, displayFormat, {locale: this.locale}); + } + + addCalendarYears(date: Date, years: number): Date { + return addYears(date, years); + } + + addCalendarMonths(date: Date, months: number): Date { + return addMonths(date, months); + } + + addCalendarDays(date: Date, days: number): Date { + return addDays(date, days); + } + + toIso8601(date: Date): string { + return formatISO(date, {representation: 'date'}); + } + + /** + * Returns the given value if given a valid Date or null. Deserializes valid ISO 8601 strings + * (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Returns an + * invalid date for all other values. + */ + override deserialize(value: any): Date | null { + if (typeof value === 'string') { + if (!value) { + return null; + } + const date = parseISO(value); + if (this.isValid(date)) { + return date; + } + } + return super.deserialize(value); + } + + isDateInstance(obj: any): boolean { + return isDate(obj); + } + + isValid(date: Date): boolean { + return isValid(date); + } + + invalid(): Date { + return new Date(NaN); + } +} diff --git a/src/material-date-fns-adapter/adapter/date-fns-formats.ts b/src/material-date-fns-adapter/adapter/date-fns-formats.ts new file mode 100644 index 000000000000..e067ad83a9c8 --- /dev/null +++ b/src/material-date-fns-adapter/adapter/date-fns-formats.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {MatDateFormats} from '@angular/material/core'; + +export const MAT_DATE_FNS_FORMATS: MatDateFormats = { + parse: { + dateInput: 'P', + }, + display: { + dateInput: 'P', + monthYearLabel: 'LLL uuuu', + dateA11yLabel: 'PP', + monthYearA11yLabel: 'LLLL uuuu', + }, +}; diff --git a/src/material-date-fns-adapter/adapter/index.ts b/src/material-date-fns-adapter/adapter/index.ts new file mode 100644 index 000000000000..4ecb2bebc014 --- /dev/null +++ b/src/material-date-fns-adapter/adapter/index.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule} from '@angular/core'; +import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core'; +import {DateFnsAdapter} from './date-fns-adapter'; +import {MAT_DATE_FNS_FORMATS} from './date-fns-formats'; + +export * from './date-fns-adapter'; +export * from './date-fns-formats'; + +@NgModule({ + providers: [{ + provide: DateAdapter, + useClass: DateFnsAdapter, + deps: [MAT_DATE_LOCALE] + }], +}) +export class DateFnsModule {} + + +@NgModule({ + imports: [DateFnsModule], + providers: [{provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS}], +}) +export class MatDateFnsModule {} diff --git a/src/material-date-fns-adapter/esbuild-amd.bzl b/src/material-date-fns-adapter/esbuild-amd.bzl new file mode 100644 index 000000000000..7490ac8369aa --- /dev/null +++ b/src/material-date-fns-adapter/esbuild-amd.bzl @@ -0,0 +1,40 @@ +load("@npm//@bazel/esbuild:index.bzl", "esbuild") + +"""Creates an ESBuild configuration file for configuring AMD output.""" + +def _create_esbuild_config(module_name): + # Workaround in ESBuild to support AMD module output. + # TODO: Remove once https://github.com/evanw/esbuild/issues/507 is fixed. + return { + "globalName": "__exports", + "banner": {"js": "define(\"%s\",[],function(){" % module_name}, + "footer": {"js": "return __exports;})"}, + } + +"""Generates an AMD bundle for the specified entry-point with the given AMD module name.""" + +def esbuild_amd(name, entry_point, module_name, testonly, deps): + native.genrule( + name = "%s_config" % name, + outs = ["%s_config.json" % name], + cmd = """echo '%s' > $@""" % json.encode(_create_esbuild_config(module_name)), + testonly = testonly, + ) + + esbuild( + name = "%s_bundle" % name, + testonly = testonly, + deps = deps, + minify = True, + sourcemap = "inline", + platform = "browser", + target = "es2015", + entry_point = entry_point, + args_file = "%s_config.json" % name, + ) + + native.filegroup( + name = name, + testonly = testonly, + srcs = ["%s_bundle" % name], + ) diff --git a/src/material-date-fns-adapter/index.ts b/src/material-date-fns-adapter/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material-date-fns-adapter/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/material-date-fns-adapter/package.json b/src/material-date-fns-adapter/package.json new file mode 100644 index 000000000000..b91cadd1a3e5 --- /dev/null +++ b/src/material-date-fns-adapter/package.json @@ -0,0 +1,33 @@ +{ + "name": "@angular/material-date-fns-adapter", + "version": "0.0.0-PLACEHOLDER", + "description": "Angular Material date-fns Adapter", + "repository": { + "type": "git", + "url": "https://github.com/angular/components.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/components/issues" + }, + "homepage": "https://github.com/angular/components#readme", + "peerDependencies": { + "@angular/material": "0.0.0-PLACEHOLDER", + "@angular/core": "0.0.0-NG", + "date-fns": "^2.23.0" + }, + "dependencies": { + "tslib": "0.0.0-TSLIB" + }, + "ng-update": { + "packageGroup": [ + "@angular/material", + "@angular/cdk", + "@angular/material-date-fns-adapter" + ] + }, + "sideEffects": false, + "publishConfig": { + "registry":"https://wombat-dressing-room.appspot.com" + } +} diff --git a/src/material-date-fns-adapter/public-api.ts b/src/material-date-fns-adapter/public-api.ts new file mode 100644 index 000000000000..c9246d3a07b8 --- /dev/null +++ b/src/material-date-fns-adapter/public-api.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './adapter/index'; diff --git a/src/material-date-fns-adapter/tsconfig-tests.json b/src/material-date-fns-adapter/tsconfig-tests.json new file mode 100644 index 000000000000..80e4ffa3e0d7 --- /dev/null +++ b/src/material-date-fns-adapter/tsconfig-tests.json @@ -0,0 +1,29 @@ +{ + "extends": "../bazel-tsconfig-build.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "../../dist/packages/material-date-fns-adapter", + "rootDir": ".", + "rootDirs": [ + ".", + "../../dist/packages/material-date-fns-adapter" + ], + "importHelpers": false, + "module": "umd", + "target": "es5", + "types": ["jasmine"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { + "@angular/material/*": ["../../dist/packages/material/*"] + } + }, + "include": [ + "**/*.ts", + "**/*.spec.ts", + "../dev-mode-types.d.ts" + ], + "exclude": [ + "**/*.e2e.spec.ts" + ] +} diff --git a/src/material-date-fns-adapter/tsconfig.json b/src/material-date-fns-adapter/tsconfig.json new file mode 100644 index 000000000000..a8acc041c9ca --- /dev/null +++ b/src/material-date-fns-adapter/tsconfig.json @@ -0,0 +1,14 @@ +// Configuration for IDEs only. +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "baseUrl": ".", + "paths": { + "@angular/cdk/*": ["../cdk/*"], + "@angular/material/*": ["../material/*"], + "@angular/material": ["../material/public-api.ts"] + } + }, + "include": ["./**/*.ts", "../dev-mode-types.d.ts"] +} diff --git a/src/material/core/datetime/date-adapter.ts b/src/material/core/datetime/date-adapter.ts index 995cb9a7a34b..d9d00396e5f7 100644 --- a/src/material/core/datetime/date-adapter.ts +++ b/src/material/core/datetime/date-adapter.ts @@ -10,20 +10,20 @@ import {inject, InjectionToken, LOCALE_ID} from '@angular/core'; import {Observable, Subject} from 'rxjs'; /** InjectionToken for datepicker that can be used to override default locale code. */ -export const MAT_DATE_LOCALE = new InjectionToken('MAT_DATE_LOCALE', { +export const MAT_DATE_LOCALE = new InjectionToken<{}>('MAT_DATE_LOCALE', { providedIn: 'root', factory: MAT_DATE_LOCALE_FACTORY, }); /** @docs-private */ -export function MAT_DATE_LOCALE_FACTORY(): string { +export function MAT_DATE_LOCALE_FACTORY(): {} { return inject(LOCALE_ID); } /** Adapts type `D` to be usable as a date by cdk-based components that work with dates. */ -export abstract class DateAdapter { +export abstract class DateAdapter { /** The locale to use for all dates. */ - protected locale: any; + protected locale: L; protected readonly _localeChanges = new Subject(); /** A stream that emits when the locale changes. */ @@ -228,7 +228,7 @@ export abstract class DateAdapter { * Sets the locale used for all dates. * @param locale The new locale. */ - setLocale(locale: any) { + setLocale(locale: L) { this.locale = locale; this._localeChanges.next(); } diff --git a/src/material/datepicker/datepicker.md b/src/material/datepicker/datepicker.md index 4d0f6241efac..c4dbaa90ec60 100644 --- a/src/material/datepicker/datepicker.md +++ b/src/material/datepicker/datepicker.md @@ -325,6 +325,29 @@ The easiest way to ensure this is to import one of the provided date modules: +`MatDateFnsModule` (installed via `@angular/material-date-fns-adapter`) + + + + + + + + + + + + + + + + + + + + +
Date typeDate
Supported localesSee project for details
Dependenciesdate-fns
Import from@angular/material-date-fns-adapter
+ `MatLuxonDateModule` (installed via `@angular/material-luxon-adapter`) diff --git a/src/material/package.json b/src/material/package.json index 595337a36498..c1ae602b368d 100644 --- a/src/material/package.json +++ b/src/material/package.json @@ -35,7 +35,8 @@ "@angular/material", "@angular/cdk", "@angular/material-moment-adapter", - "@angular/material-luxon-adapter" + "@angular/material-luxon-adapter", + "@angular/material-date-fns-adapter" ] }, "sideEffects": false, diff --git a/test/karma.conf.js b/test/karma.conf.js index 367441e0cedd..878bf51a941b 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -58,6 +58,10 @@ module.exports = config => { {pattern: 'dist/karma-system-config.js', included: true, watched: false}, {pattern: 'test/karma-test-shim.js', included: true, watched: false}, + // We transpile the date-fns bundles to AMD manually since they only ship esm bundles. + {pattern: 'dist/amd_date_fns.js', included: false, watched: false}, + {pattern: 'dist/amd_date_fns_locales.js', included: false, watched: false}, + // Needed for exposing the RxJS operators through the RxJS UMD bundle. This // is done for performance reasons since fetching individual files is slow. {pattern: 'tools/system-rxjs-operators.js', included: false, watched: false}, diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index f5b9b37b9036..f30020961771 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -7,6 +7,7 @@ import { materialPackage, momentAdapterPackage, luxonAdapterPackage, + dateFnsAdapterPackage, youTubePlayerPackage } from './packages'; @@ -17,6 +18,7 @@ createPackageBuildTasks(materialPackage); createPackageBuildTasks(materialExperimentalPackage); createPackageBuildTasks(momentAdapterPackage); createPackageBuildTasks(luxonAdapterPackage); +createPackageBuildTasks(dateFnsAdapterPackage); createPackageBuildTasks(youTubePlayerPackage); createPackageBuildTasks(googleMapsPackage); diff --git a/tools/gulp/packages.ts b/tools/gulp/packages.ts index 1573c9d4a58e..e9b438823f20 100644 --- a/tools/gulp/packages.ts +++ b/tools/gulp/packages.ts @@ -9,3 +9,5 @@ export const materialExperimentalPackage = new BuildPackage('material-experiment [cdkPackage, cdkExperimentalPackage, materialPackage]); export const momentAdapterPackage = new BuildPackage('material-moment-adapter', [materialPackage]); export const luxonAdapterPackage = new BuildPackage('material-luxon-adapter', [materialPackage]); +export const dateFnsAdapterPackage = + new BuildPackage('material-date-fns-adapter', [materialPackage]); diff --git a/tools/gulp/tasks/unit-test.ts b/tools/gulp/tasks/unit-test.ts index 3b18e989bfbd..372e9dcfccc0 100644 --- a/tools/gulp/tasks/unit-test.ts +++ b/tools/gulp/tasks/unit-test.ts @@ -19,6 +19,22 @@ task(':test:build-system-config', done => { done(); }); +/** Compiles the esm bundles from date-fns to AMD so that we can load them in Karma. */ +task(':test:build-date-fns', done => { + shelljs.cd(buildConfig.projectDir); + const bazelGenfilesDir = shelljs.exec('yarn -s bazel info bazel-genfiles').stdout.trim(); + + ['amd_date_fns', 'amd_date_fns_locales'].forEach(target => { + const outputPath = join(buildConfig.outputDir, `${target}.js`); + const bundlePath = join(bazelGenfilesDir, `src/material-date-fns-adapter/${target}_bundle.js`); + shelljs.exec(`yarn -s bazel build //src/material-date-fns-adapter:${target}`); + shelljs.cp(bundlePath, outputPath); + shelljs.chmod('u+w', outputPath); + }); + + done(); +}); + /** Builds everything that is necessary for karma. */ task(':test:build', series( 'clean', @@ -29,7 +45,9 @@ task(':test:build', series( 'youtube-player:build-no-bundles', 'material-moment-adapter:build-no-bundles', 'material-luxon-adapter:build-no-bundles', + 'material-date-fns-adapter:build-no-bundles', 'google-maps:build-no-bundles', + ':test:build-date-fns', ':test:build-system-config' )); diff --git a/tools/linker-process/linker-process.ts b/tools/linker-process/linker-process.ts index 42c2bc4b82c5..61d1db328857 100644 --- a/tools/linker-process/linker-process.ts +++ b/tools/linker-process/linker-process.ts @@ -1,5 +1,5 @@ import {readFileSync, writeFileSync} from 'fs'; -import {join} from 'path'; +import {join, extname} from 'path'; // These imports to `@angular/compiler-cli` need to explicitly specify the `.js` extension as // otherwise the Bazel NodeJS module resolution would end up resolving the ESM2015 `.mjs` files. @@ -81,6 +81,14 @@ function generateAmdModuleMappingFile(mappings: Map): string { /** Processes the given file with the Angular linker plugin. */ function processFileWithLinker(diskFilePath: string, fileContent: string): string { + const fileExtension = extname(diskFilePath); + + // If the input file is not a JavaScript file, do not process it with the linker + // Babel plugin and return the original file content. + if (fileExtension !== '.js' && fileExtension !== '.mjs') { + return fileContent; + } + // We run the linker with JIT mode so that the processed Angular declarations could be // run within unit tests that rely on JIT information to be available. const linkerPlugin = createEs2015LinkerPlugin({fileSystem, logger, linkerJitMode: true}); diff --git a/tools/public_api_guard/material/core.md b/tools/public_api_guard/material/core.md index b1bfb7dd03ef..1c71c84fbab8 100644 --- a/tools/public_api_guard/material/core.md +++ b/tools/public_api_guard/material/core.md @@ -95,7 +95,7 @@ export type CanUpdateErrorStateCtor = Constructor & Abstrac export function _countGroupLabelsBeforeOption(optionIndex: number, options: QueryList, optionGroups: QueryList): number; // @public -export abstract class DateAdapter { +export abstract class DateAdapter { abstract addCalendarDays(date: D, days: number): D; abstract addCalendarMonths(date: D, months: number): D; abstract addCalendarYears(date: D, years: number): D; @@ -119,13 +119,13 @@ export abstract class DateAdapter { abstract invalid(): D; abstract isDateInstance(obj: any): boolean; abstract isValid(date: D): boolean; - protected locale: any; + protected locale: L; readonly localeChanges: Observable; // (undocumented) protected readonly _localeChanges: Subject; abstract parse(value: any, parseFormat: any): D | null; sameDate(first: D | null, second: D | null): boolean; - setLocale(locale: any): void; + setLocale(locale: L): void; abstract today(): D; abstract toIso8601(date: D): string; } @@ -181,10 +181,10 @@ export type HasTabIndexCtor = Constructor & AbstractConstructor; // @public -export const MAT_DATE_LOCALE: InjectionToken; +export const MAT_DATE_LOCALE: InjectionToken<{}>; // @public -export function MAT_DATE_LOCALE_FACTORY(): string; +export function MAT_DATE_LOCALE_FACTORY(): {}; // @public (undocumented) export const MAT_NATIVE_DATE_FORMATS: MatDateFormats; diff --git a/tools/release/changelog.ts b/tools/release/changelog.ts index 98653f07b2a0..b76d86797c6c 100644 --- a/tools/release/changelog.ts +++ b/tools/release/changelog.ts @@ -33,6 +33,7 @@ const orderedChangelogPackages = [ 'youtube-player', 'material-moment-adapter', 'material-luxon-adapter', + 'material-date-fns-adapter', 'cdk-experimental', 'material-experimental', ]; diff --git a/tools/release/release-output/release-packages.ts b/tools/release/release-output/release-packages.ts index 43d2f65c6c9a..bcd7f66f300b 100644 --- a/tools/release/release-output/release-packages.ts +++ b/tools/release/release-output/release-packages.ts @@ -8,4 +8,5 @@ export const releasePackages = [ 'material-experimental', 'material-moment-adapter', 'material-luxon-adapter', + 'material-date-fns-adapter', ]; diff --git a/tools/system-config-tmpl.js b/tools/system-config-tmpl.js index 16e9c3cf3e45..782270351e12 100644 --- a/tools/system-config-tmpl.js +++ b/tools/system-config-tmpl.js @@ -33,13 +33,17 @@ var nodeModulesPath = '$NODE_MODULES_BASE_PATH'; /** Path mappings that will be registered in SystemJS. */ var pathMapping = { 'tslib': 'node:tslib/tslib.js', - 'moment': 'node:moment/min/moment-with-locales.min.js', 'luxon': 'node:luxon/build/amd/luxon.js', + 'moment': 'node:moment/min/moment-with-locales.min.js', 'moment/locale': 'node:moment/locale', 'kagekiri': 'node:kagekiri/dist/kagekiri.umd.min.js', 'rxjs': 'node:rxjs/bundles/rxjs.umd.min.js', 'rxjs/operators': 'tools/system-rxjs-operators.js', + + // These path mappings are only for the legacy Karma setup. + 'date-fns': 'dist/amd_date_fns.js', + 'date-fns/locale': 'dist/amd_date_fns_locales.js', }; /** Package configurations that will be used in SystemJS. */ @@ -128,6 +132,7 @@ function setupLocalReleasePackages() { configureEntryPoint('material-experimental'); configureEntryPoint('material-moment-adapter'); configureEntryPoint('material-luxon-adapter'); + configureEntryPoint('material-date-fns-adapter'); configureEntryPoint('google-maps'); configureEntryPoint('youtube-player'); diff --git a/tsconfig.json b/tsconfig.json index 06264dd9e5b6..efe543ab58c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ "@angular/cdk-experimental": ["./src/cdk-experimental"], "@angular/material-moment-adapter": ["./src/material-moment-adapter"], "@angular/material-luxon-adapter": ["./src/material-luxon-adapter"], + "@angular/material-date-fns-adapter": ["./src/material-date-fns-adapter"], "@angular/components-examples": ["./src/components-examples"], "@angular/components-examples/*": ["./src/components-examples/*"] } diff --git a/yarn.lock b/yarn.lock index 1b4d653e16b6..36ee4b897474 100644 --- a/yarn.lock +++ b/yarn.lock @@ -481,6 +481,11 @@ source-map-support "0.5.9" tsutils "2.27.2" +"@bazel/esbuild@4.0.0-beta.0": + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@bazel/esbuild/-/esbuild-4.0.0-beta.0.tgz#ff75447b18bc0d56b376d7e44095d9ca9b904583" + integrity sha512-4AxL8IhyeyeTH0fr1XFfdd1ls/AnsiEu1oBXxoplb0ar88pRrdl0UjCUgLylWj75uIcQsqu/l3Xv7qOfDSXWsQ== + "@bazel/ibazel@0.15.10": version "0.15.10" resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f" @@ -4485,6 +4490,11 @@ data-uri-to-buffer@3: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== +date-fns@^2.23.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9" + integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA== + date-format@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"