From 8f06b29550f4b1cddc9d7a57cb3dd97cd858dec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Feb 2019 12:41:31 +0100 Subject: [PATCH 1/2] Type check with Flow --- .eslintrc.json | 4 ++- .flowconfig | 9 +++++++ package-lock.json | 18 +++++++++----- package.json | 10 +++++--- prettier.config.js | 2 ++ rollup.config.js | 8 +++--- src/extended-time-element.js | 21 ++++++++++------ src/index.js | 12 ++++++--- src/local-time-element.js | 48 +++++++++++++++++------------------- src/relative-time-element.js | 4 ++- src/relative-time.js | 10 +++++--- src/time-ago-element.js | 15 +++++------ src/time-until-element.js | 15 +++++------ src/utils.js | 21 +++++++++------- 14 files changed, 118 insertions(+), 79 deletions(-) create mode 100644 .flowconfig diff --git a/.eslintrc.json b/.eslintrc.json index ace6821..81a62c8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,13 +1,15 @@ { "extends": [ "plugin:github/browser", - "plugin:github/es6" + "plugin:github/es6", + "plugin:github/flow" ], "parser": "babel-eslint", "overrides": [ { "files": "test/**/*.js", "rules": { + "flowtype/require-valid-file-annotation": "off", "github/unescaped-html-literal": "off" } } diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..60e7050 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,9 @@ +[ignore] + +[include] + +[libs] + +[options] + +[lints] diff --git a/package-lock.json b/package-lock.json index fa334bc..c717cc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1070,9 +1070,9 @@ "dev": true }, "babel-preset-github": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/babel-preset-github/-/babel-preset-github-2.1.1.tgz", - "integrity": "sha512-lIW8MTBJ2qveRtyWSYLTQt3Dh3Foliv5dQP5nY+/9pf4kXpezCTx8fl0w31IWAT/+VtPAbYVdk+2mfyaDP34Yw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/babel-preset-github/-/babel-preset-github-2.1.2.tgz", + "integrity": "sha512-g93xWa+3/OIhtO+55mlYYi8o6pSFqwAbAdDX9eql1zpCRbzOb1mCuqpMESdPuQ/HVQM5sYvg1x7Re/+ESkUhbg==", "dev": true, "requires": { "@babel/plugin-proposal-class-properties": "^7.1.0", @@ -2641,6 +2641,12 @@ "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", "dev": true }, + "flow-bin": { + "version": "0.93.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.93.0.tgz", + "integrity": "sha512-p8yq4ocOlpyJgOEBEj0v0GzCP25c9WP0ilFQ8hXSbrTR7RPKuR+Whr+OitlVyp8ocdX0j1MrIwQ8x28dacy1pg==", + "dev": true + }, "follow-redirects": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", @@ -5069,9 +5075,9 @@ } }, "regenerator-transform": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", - "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.4.tgz", + "integrity": "sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A==", "dev": true, "requires": { "private": "^0.1.6" diff --git a/package.json b/package.json index 717a4c5..60c98ee 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "scripts": { "clean": "rm -rf ./dist ./node_modules", - "lint": "eslint .", + "lint": "github-lint", "build": "rollup -c", "prepublishOnly": "npm run pretest", "pretest": "npm run lint && npm run build", @@ -15,11 +15,11 @@ "repository": "github/time-elements", "devDependencies": { "@babel/core": "^7.2.2", - "@babel/plugin-proposal-export-default-from": "^7.2.0", - "babel-preset-github": "^2.1.1", + "babel-preset-github": "^2.1.2", "chai": "^4.2.0", "eslint": "^5.12.0", "eslint-plugin-github": "^1.9.0", + "flow-bin": "^0.93.0", "karma": "^3.1.4", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", @@ -30,5 +30,7 @@ "rollup-plugin-babel": "^4.2.0", "webcomponents.js": "^0.7.23" }, - "eslintIgnore": ["dist/"] + "eslintIgnore": [ + "dist/" + ] } diff --git a/prettier.config.js b/prettier.config.js index 7d9937d..973f1a7 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1 +1,3 @@ +/* @flow strict */ + module.exports = require('eslint-plugin-github/prettier.config') diff --git a/rollup.config.js b/rollup.config.js index 64e4595..de4eafd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,5 @@ +/* @flow strict */ + import babel from 'rollup-plugin-babel' const pkg = require('./package.json') @@ -7,8 +9,7 @@ export default [ input: 'src/index.js', plugins: [ babel({ - presets: ['github'], - plugins: ['@babel/plugin-proposal-export-default-from'] + presets: ['github'] }) ], output: { @@ -21,8 +22,7 @@ export default [ input: 'src/index.js', plugins: [ babel({ - presets: ['github'], - plugins: ['@babel/plugin-proposal-export-default-from'] + presets: ['github'] }) ], output: { diff --git a/src/extended-time-element.js b/src/extended-time-element.js index e67618d..fbbc4d9 100644 --- a/src/extended-time-element.js +++ b/src/extended-time-element.js @@ -1,6 +1,10 @@ +/* @flow strict */ + import {makeFormatter} from './utils' export default class ExtendedTimeElement extends HTMLElement { + _date: ?Date + static get observedAttributes() { return ['datetime', 'day', 'format', 'hour', 'minute', 'month', 'second', 'title', 'weekday', 'year'] } @@ -8,7 +12,7 @@ export default class ExtendedTimeElement extends HTMLElement { // Internal: Refresh the time element's formatted date when an attribute changes. // // Returns nothing. - attributeChangedCallback(attrName, oldValue, newValue) { + attributeChangedCallback(attrName: string, oldValue: string, newValue: string) { if (attrName === 'datetime') { const millis = Date.parse(newValue) this._date = isNaN(millis) ? null : new Date(millis) @@ -30,26 +34,27 @@ export default class ExtendedTimeElement extends HTMLElement { // value takes precedence over this custom format. // // Returns a formatted time String. - getFormattedTitle() { - if (!this._date) { - return - } + getFormattedTitle(): ?string { + const date = this._date + if (!date) return const formatter = titleFormatter() if (formatter) { - return formatter.format(this._date) + return formatter.format(date) } else { try { - return this._date.toLocaleString() + return date.toLocaleString() } catch (e) { if (e instanceof RangeError) { - return this._date.toString() + return date.toString() } else { throw e } } } } + + getFormattedDate(): ?string {} } const titleFormatter = makeFormatter({ diff --git a/src/index.js b/src/index.js index d146faa..a3526c9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,8 @@ -export LocalTimeElement from './local-time-element' -export RelativeTimeElement from './relative-time-element' -export TimeAgoElement from './time-ago-element' -export TimeUntilElement from './time-until-element' +/* @flow strict */ + +import LocalTimeElement from './local-time-element' +import RelativeTimeElement from './relative-time-element' +import TimeAgoElement from './time-ago-element' +import TimeUntilElement from './time-until-element' + +export {LocalTimeElement, RelativeTimeElement, TimeAgoElement, TimeUntilElement} diff --git a/src/local-time-element.js b/src/local-time-element.js index e37c29a..c3c0954 100644 --- a/src/local-time-element.js +++ b/src/local-time-element.js @@ -1,10 +1,12 @@ +/* @flow strict */ + import {strftime, makeFormatter, isDayFirst} from './utils' import ExtendedTimeElement from './extended-time-element' const formatters = new WeakMap() export default class LocalTimeElement extends ExtendedTimeElement { - attributeChangedCallback(attrName, oldValue, newValue) { + attributeChangedCallback(attrName: string, oldValue: string, newValue: string) { if (attrName === 'hour' || attrName === 'minute' || attrName === 'second' || attrName === 'time-zone-name') { formatters.delete(this) } @@ -27,13 +29,12 @@ export default class LocalTimeElement extends ExtendedTimeElement { // second - "numeric", "2-digit" // // Returns a formatted time String. - getFormattedDate() { - if (!this._date) { - return - } + getFormattedDate(): ?string { + const d = this._date + if (!d) return - const date = formatDate(this) || '' - const time = formatTime(this) || '' + const date = formatDate(this, d) || '' + const time = formatTime(this, d) || '' return `${date} ${time}`.trim() } } @@ -48,7 +49,7 @@ export default class LocalTimeElement extends ExtendedTimeElement { // el - The local-time element to format. // // Returns a date String or null if no date formats are provided. -function formatDate(el) { +function formatDate(el: Element, date: Date) { // map attribute values to strftime const props = { weekday: { @@ -80,7 +81,7 @@ function formatDate(el) { format = format.replace(/(\s,)|(,\s$)/, '') // squeeze spaces from final string - return strftime(el._date, format) + return strftime(date, format) .replace(/\s+/, ' ') .trim() } @@ -91,21 +92,18 @@ function formatDate(el) { // el - The local-time element to format. // // Returns a time String or null if no time formats are provided. -function formatTime(el) { - // retrieve format settings from attributes - const options = { - hour: el.getAttribute('hour'), - minute: el.getAttribute('minute'), - second: el.getAttribute('second'), - timeZoneName: el.getAttribute('time-zone-name') - } +function formatTime(el: Element, date: Date) { + const options: Intl$DateTimeFormatOptions = {} - // Remove unset format attributes. - for (const opt in options) { - if (!options[opt]) { - delete options[opt] - } - } + // retrieve format settings from attributes + const hour = el.getAttribute('hour') + if (hour === 'numeric' || hour === '2-digit') options.hour = hour + const minute = el.getAttribute('minute') + if (minute === 'numeric' || minute === '2-digit') options.minute = minute + const second = el.getAttribute('second') + if (second === 'numeric' || second === '2-digit') options.second = second + const tz = el.getAttribute('time-zone-name') + if (tz === 'short' || tz === 'long') options.timeZoneName = tz // No time format attributes provided. if (Object.keys(options).length === 0) { @@ -121,11 +119,11 @@ function formatTime(el) { const formatter = factory() if (formatter) { // locale-aware formatting of 24 or 12 hour times - return formatter.format(el._date) + return formatter.format(date) } else { // fall back to strftime for non-Intl browsers const timef = options.second ? '%H:%M:%S' : '%H:%M' - return strftime(el._date, timef) + return strftime(date, timef) } } diff --git a/src/relative-time-element.js b/src/relative-time-element.js index 772f887..e0c6d7f 100644 --- a/src/relative-time-element.js +++ b/src/relative-time-element.js @@ -1,3 +1,5 @@ +/* @flow strict */ + import RelativeTime from './relative-time' import ExtendedTimeElement from './extended-time-element' @@ -45,7 +47,7 @@ function updateNowElements() { let time, i, len for (i = 0, len = nowElements.length; i < len; i++) { time = nowElements[i] - time.textContent = time.getFormattedDate() + time.textContent = time.getFormattedDate() || '' } } diff --git a/src/relative-time.js b/src/relative-time.js index 497e7fd..35c712a 100644 --- a/src/relative-time.js +++ b/src/relative-time.js @@ -1,7 +1,11 @@ +/* @flow strict */ + import {strftime, makeFormatter, isDayFirst, isThisYear, isYearSeparator} from './utils' export default class RelativeTime { - constructor(date) { + date: Date + + constructor(date: Date) { this.date = date } @@ -50,7 +54,7 @@ export default class RelativeTime { return this.timeAgoFromMs(ms) } - timeAgoFromMs(ms) { + timeAgoFromMs(ms: number) { const sec = Math.round(ms / 1000) const min = Math.round(sec / 60) const hr = Math.round(min / 60) @@ -112,7 +116,7 @@ export default class RelativeTime { return this.timeUntilFromMs(ms) } - timeUntilFromMs(ms) { + timeUntilFromMs(ms: number) { const sec = Math.round(ms / 1000) const min = Math.round(sec / 60) const hr = Math.round(min / 60) diff --git a/src/time-ago-element.js b/src/time-ago-element.js index 068ed0b..7a5c286 100644 --- a/src/time-ago-element.js +++ b/src/time-ago-element.js @@ -1,15 +1,16 @@ +/* @flow strict */ + import RelativeTime from './relative-time' import RelativeTimeElement from './relative-time-element' export default class TimeAgoElement extends RelativeTimeElement { getFormattedDate() { - if (this._date) { - const format = this.getAttribute('format') - if (format === 'micro') { - return new RelativeTime(this._date).microTimeAgo() - } else { - return new RelativeTime(this._date).timeAgo() - } + const format = this.getAttribute('format') + if (!this._date) return + if (format === 'micro') { + return new RelativeTime(this._date).microTimeAgo() + } else { + return new RelativeTime(this._date).timeAgo() } } } diff --git a/src/time-until-element.js b/src/time-until-element.js index 0949e50..ed7942d 100644 --- a/src/time-until-element.js +++ b/src/time-until-element.js @@ -1,15 +1,16 @@ +/* @flow strict */ + import RelativeTime from './relative-time' import RelativeTimeElement from './relative-time-element' export default class TimeUntilElement extends RelativeTimeElement { getFormattedDate() { - if (this._date) { - const format = this.getAttribute('format') - if (format === 'micro') { - return new RelativeTime(this._date).microTimeUntil() - } else { - return new RelativeTime(this._date).timeUntil() - } + const format = this.getAttribute('format') + if (!this._date) return + if (format === 'micro') { + return new RelativeTime(this._date).microTimeUntil() + } else { + return new RelativeTime(this._date).timeUntil() } } } diff --git a/src/utils.js b/src/utils.js index 3015980..b97bb20 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,5 @@ +/* @flow strict */ + const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const months = [ 'January', @@ -18,7 +20,7 @@ function pad(num) { return `0${num}`.slice(-2) } -export function strftime(time, formatString) { +export function strftime(time: Date, formatString: string): string { const day = time.getDay() const date = time.getDate() const month = time.getMonth() @@ -45,16 +47,16 @@ export function strftime(time, formatString) { case 'd': return pad(date) case 'e': - return date + return String(date) case 'H': return pad(hour) case 'I': return pad(strftime(time, '%l')) case 'l': if (hour === 0 || hour === 12) { - return 12 + return String(12) } else { - return (hour + 12) % 12 + return String((hour + 12) % 12) } case 'm': return pad(month + 1) @@ -75,11 +77,11 @@ export function strftime(time, formatString) { case 'S': return pad(second) case 'w': - return day + return String(day) case 'y': return pad(year % 100) case 'Y': - return year + return String(year) case 'Z': match = time.toString().match(/\((\w+)\)$/) return match ? match[1] : '' @@ -87,10 +89,11 @@ export function strftime(time, formatString) { match = time.toString().match(/\w([+-]\d\d\d\d) /) return match ? match[1] : '' } + return '' }) } -export function makeFormatter(options) { +export function makeFormatter(options: Intl$DateTimeFormatOptions): () => ?Intl$DateTimeFormat { let format return function() { if (format) return format @@ -115,7 +118,7 @@ const dayFirstFormatter = makeFormatter({day: 'numeric', month: 'short'}) // for en-US. // // Returns true if the day appears before the month. -export function isDayFirst() { +export function isDayFirst(): boolean { if (dayFirst !== null) { return dayFirst } @@ -157,7 +160,7 @@ export function isYearSeparator() { // date - The Date to test. // // Returns true if it's this year. -export function isThisYear(date) { +export function isThisYear(date: Date) { const now = new Date() return now.getUTCFullYear() === date.getUTCFullYear() } From 721c7b49fa211d51e89497afd5da819364c5c238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Feb 2019 12:45:21 +0100 Subject: [PATCH 2/2] Keep all eslint config together --- .eslintrc.json | 10 ++++++++++ test/.eslintrc.json | 15 --------------- 2 files changed, 10 insertions(+), 15 deletions(-) delete mode 100644 test/.eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json index 81a62c8..10167dd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,16 @@ "flowtype/require-valid-file-annotation": "off", "github/unescaped-html-literal": "off" } + }, + { + "files": "test/**/*.js", + "excludedFiles": "test/karma.config.js", + "env": { + "mocha": true + }, + "globals": { + "assert": true + } } ] } diff --git a/test/.eslintrc.json b/test/.eslintrc.json deleted file mode 100644 index 6b4d521..0000000 --- a/test/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "plugins": [ - "github" - ], - "extends": [ - "plugin:github/recommended" - ], - "env": { - "browser": true, - "mocha": true - }, - "globals": { - "assert": true - } -}