Migrating from moment to date-fns
I got a lot of positive feedback from my recent article on how I cut my webpack bundle size in half, and a common refrain was how folks found themselves in the same position as I did in terms of deciding to migrate off of moment.js
.
A handful of folks suggested date-fns as an alternative. date-fns
has a relatively clumsy name but offered excatly what I was looking for:
- Modularity and tree-shaking compatability.
- Use of native dates
- TypeScript support
So I decided to take the plunge and try migrating: the surface area of my use of moment wasn’t that large, so I hoped it would be relatively painless.
(Spoiler alert: it was.)
Here’s what the diff looked like:
- assets/components/DateTimePicker.vue +2 -4
- assets/components/DraftLoadingNotice.vue +3 -3
- assets/screens/Write.vue +4 -3
- assets/store/actions.ts +3 -0
- assets/utils.ts +7 -3
- emails/services/email_analytics_fetcher.py +1 -1
- package.json +1 -1
- webpack-stats.json +1 -1
- yarn.lock +4 -4
@@ -7,16 +7,14 @@
|
|
7
7
|
</template>
|
8
8
|
|
9
9
|
<script>
|
10
|
-
import
|
10
|
+
import format from 'date-fns/format';
|
11
11
|
|
12
12
|
export default {
|
13
13
|
props: ['value'],
|
14
14
|
|
15
15
|
computed: {
|
16
16
|
formattedValue() {
|
17
|
-
return this.value
|
17
|
+
return this.value ? format(this.value, 'MM/DD/YYYY @ h:mm a') : null;
|
18
|
-
? moment(this.value).format('MM/DD/YYYY @ h:mm a')
|
19
|
-
: null;
|
20
18
|
},
|
21
19
|
},
|
22
20
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
You worked on a
|
5
5
|
<span v-if="latestDraft.subject">draft entitled <strong> latestDraft.subject </strong></span>
|
6
6
|
<span v-else>untitled draft</span>
|
7
|
-
latestDraftAgo
|
7
|
+
latestDraftAgo ago.
|
8
8
|
<a href="#" @click="loadDraft"><strong>Continue writing</strong></a>.
|
9
9
|
</div>
|
10
10
|
<div class="actions">
|
@@ -16,7 +16,7 @@
|
|
16
16
|
</template>
|
17
17
|
|
18
18
|
<script>
|
19
|
-
import
|
19
|
+
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
20
20
|
|
21
21
|
import { mapState } from 'vuex';
|
22
22
|
import { DRAFTS } from 'store/state_types';
|
@@ -41,7 +41,7 @@
|
|
41
41
|
computed: {
|
42
42
|
latestDraftAgo() {
|
43
43
|
return this.latestDraft
|
44
|
-
?
|
44
|
+
? distanceInWordsToNow(this.latestDraft.modification_date)
|
45
45
|
: null;
|
46
46
|
},
|
47
47
|
latestDraft() {
|
@@ -99,7 +99,7 @@
|
|
99
99
|
</template>
|
100
100
|
|
101
101
|
<script lang="ts">
|
102
|
-
import
|
102
|
+
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
103
103
|
import { mapState } from 'vuex';
|
104
104
|
|
105
105
|
import { USER } from 'store/state_types';
|
@@ -138,10 +138,11 @@ export default {
|
|
138
138
|
body: computeWorkingEmail('body'),
|
139
139
|
draftId: computeWorkingEmail('draftId'),
|
140
140
|
timeUntilPublish() {
|
141
|
-
|
141
|
+
const utcOffset = (new Date()).getTimezoneOffset() * 60 * 1000;
|
142
|
+
if (Date.parse(this.publishDate) - utcOffset < Date.now()) {
|
142
143
|
return 'immediately';
|
143
144
|
}
|
144
|
-
return
|
145
|
+
return distanceInWordsToNow(Date.parse(this.publishDate) - utcOffset);
|
145
146
|
},
|
146
147
|
needsBilling() {
|
147
148
|
return this.user.settings.needs_billing_information;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import each from 'lodash/each';
|
2
2
|
import marked from 'marked';
|
3
|
-
import
|
3
|
+
import format from 'date-fns/format';
|
4
|
+
import differenceInDays from 'date-fns/difference_in_days';
|
4
5
|
import Noty from 'noty';
|
5
6
|
|
6
7
|
declare const SITE_URL;
|
@@ -71,7 +72,7 @@ const extractLinksFromMarkdown = markdownString => {
|
|
71
72
|
};
|
72
73
|
|
73
74
|
/* Given a raw date timestamp, format it nicely. */
|
74
|
-
const formatDate = date =>
|
75
|
+
const formatDate = date => format(date, 'MM/DD/YYYY');
|
75
76
|
|
76
77
|
const renderMarkdownWithAllLinksAsTargetBlank = markdownString => {
|
77
78
|
// Create a custom renderer to always use target=_blank.
|
@@ -83,7 +84,10 @@ const renderMarkdownWithAllLinksAsTargetBlank = markdownString => {
|
|
83
84
|
return marked(markdownString, { renderer });
|
84
85
|
};
|
85
86
|
|
86
|
-
const daysSince = dateString =>
|
87
|
+
const daysSince = dateString => differenceInDays(
|
88
|
+
Date.now(),
|
89
|
+
Date.parse(dateString),
|
90
|
+
);
|
87
91
|
|
88
92
|
const siteTitle = 'Buttondown';
|
89
93
|
|
@@ -41,6 +41,7 @@
|
|
41
41
|
"cross-env": "^3.1.4",
|
42
42
|
"cross-spawn": "^5.0.1",
|
43
43
|
"css-loader": "^0.26.2",
|
44
|
+
"date-fns": "^1.28.5",
|
44
45
|
"diff-match-patch": "^1.0.0",
|
45
46
|
"email-validator": "^1.0.7",
|
46
47
|
"emoji-regex": "^6.4.2",
|
@@ -85,7 +86,6 @@
|
|
85
86
|
"marked": "^0.3.6",
|
86
87
|
"md5": "^2.2.1",
|
87
88
|
"mocha": "^3.2.0",
|
88
|
-
"moment": "^2.18.1",
|
89
89
|
"nightwatch": "^0.9.12",
|
90
90
|
"node-sass": "latest",
|
91
91
|
"noty": "^3.1.0-beta",
|
@@ -1 +1 @@
|
|
1
|
-
{"status":"done","chunks":{"main":[{"name":"main-
|
1
|
+
{"status":"done","publicPath":"/static/webpack_bundles/","chunks":{"zxcvbn":[{"name":"zxcvbn-376b5b3616c0f1ab29a7.js","publicPath":"/static/webpack_bundles/zxcvbn-376b5b3616c0f1ab29a7.js","path":"/Users/jmduke/workspaces/buttondown/assets/webpack_bundles/zxcvbn-376b5b3616c0f1ab29a7.js"}],"flatpickr":[{"name":"flatpickr-376b5b3616c0f1ab29a7.js","publicPath":"/static/webpack_bundles/flatpickr-376b5b3616c0f1ab29a7.js","path":"/Users/jmduke/workspaces/buttondown/assets/webpack_bundles/flatpickr-376b5b3616c0f1ab29a7.js"}],"main":[{"name":"main-376b5b3616c0f1ab29a7.js","publicPath":"/static/webpack_bundles/main-376b5b3616c0f1ab29a7.js","path":"/Users/jmduke/workspaces/buttondown/assets/webpack_bundles/main-376b5b3616c0f1ab29a7.js"}],"miscellany":[{"name":"miscellany-376b5b3616c0f1ab29a7.js","publicPath":"/static/webpack_bundles/miscellany-376b5b3616c0f1ab29a7.js","path":"/Users/jmduke/workspaces/buttondown/assets/webpack_bundles/miscellany-376b5b3616c0f1ab29a7.js"},{"name":"miscellany.css","publicPath":"/static/webpack_bundles/miscellany.css","path":"/Users/jmduke/workspaces/buttondown/assets/webpack_bundles/miscellany.css"}]},"error":"unknown-error","message":"(3,20): error TS2307: Cannot find module 'moment'."}
|
@@ -2099,6 +2099,10 @@ data-uri-to-buffer@1:
|
|
2099
2099
|
version "1.2.0"
|
2100
2100
|
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
|
2101
2101
|
|
2102
|
+
date-fns@^1.28.5:
|
2103
|
+
version "1.28.5"
|
2104
|
+
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf"
|
2105
|
+
|
2102
2106
|
date-now@^0.1.4:
|
2103
2107
|
version "0.1.4"
|
2104
2108
|
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
@@ -5142,10 +5146,6 @@ mocha@^3.2.0:
|
|
5142
5146
|
mkdirp "0.5.1"
|
5143
5147
|
supports-color "3.1.2"
|
5144
5148
|
|
5145
|
-
moment@^2.18.1:
|
5146
|
-
version "2.18.1"
|
5147
|
-
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
5148
|
-
|
5149
5149
|
ms@0.7.1:
|
5150
5150
|
version "0.7.1"
|
5151
5151
|
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
|
As you can see, nothing too crazy. A couple notes:
- Half of the stuff is straight-up API parity, like
format
adndistance_in_words
. - The only tricky part was replacing
moment.utc
, since it didn’t seem likedate-fns
had that much timezone support. (It’s also entirely possible there is a much easier way to do this that eluded me.) - Buttondown’s bundle is now 150 kilobytes thinner. 🎉
So, yeah! Check out date-fns
. It’s very nice. I would still like to take another stab at unifying Buttondown’s use of datetime (just to make migrations like this easier in the future), but I’m super happy with how pleasant (and rewarding) this process was.