File versioning package filever
automatically versions files for packaging and
distribution. File versioning is essential for browser cache busting for
Javascript, HTML, CSS, images, and other assets loaded by the browser.
filever
has two main functions:
- (Version) Hash versioned files and generate filever. Place new versioned files into an output directory. Delete any old versions of that file.
- (Replace) In the output directory (
dist
), update references in source files to versioned files.
We recommend using filever
in conjunction with watchmod and once
configured, a file change will trigger a global update of file versions and
references in source files, making development fast and painless. Compared to
other methods, filever
shines when source files refer to versioned files and
when file contents are frequently updated and changed.
filever
may be used in pipelines that involves these keywords: File
versioning, TypeScript, Sass, Go Templates, handlebars, Javascript minification,
HTML minification, and CSS minification.
The version string is a digest derived from the content of the file.
The default version is 32 bits, 8 character version with a 1 out of 4.2 billion
chance for collision. (We considered 7 characters, 1 out of 268 million
chance, but it's better to use a whole byte instead of 5.25 bytes. We consider 6
characters to be too small with a 1 out of 16 million chance of collision.)
The whole version is 12 characters long; the deliminator ~
is 1 character ,
identifier fv
is 2 characters, key/value delimiter =
is 1 character, and 8
characters for the base64 version.
app~fv=4mIbJJPq.min.js
See package path for file and URI path naming. A filever is the full file name with a file version.
General naming not specific to filever
:
Name | Example |
---|---|
Full path | e/app~fv=4mIbJJPq.min.js |
Dir | e/ |
File (filename) | app~fv=4mIbJJPq.min.js |
FileBase | app~fv=4mIbJJPq |
Extension | .min.js |
Extension base | .js |
filever
specific naming
Name | Example |
---|---|
Pathed filever | e/app~fv=4mIbJJPq.min.js |
Filever | app~fv=4mIbJJPq.min.js |
Bare path | e/app.min.js |
Bare file | app.min.js |
Bare | app |
Version | 4mIbJJPq |
Delimiter (delim) | ~fv= |
Delimited (delim'd) version | ~fv=4mIbJJPq |
Dummy version (zeroed version) | 00000000 |
Delim'd dummy Version | ~fv=00000000 |
Dummy versioned file (full dummy) | e/app~fv=00000000.min.js |
Bare path is the full path stripped of versioning.
A filever may refer to a file with a dummy version, i.e.
app~fv=00000000.min.js
is a legitimate filever.
The base name of a filever is everything before the ~
character and excludes
pathing. For the path /example/app~fv=4mIbJJPq.min.js
, the base is simply
app
with no extension or pathing.
Extension includes all sub extensions and a preceding .
. For
app~fv=4mIbJJPq.min.js.map
the extension is .min.js.map
.
The base extension is always the last extension if present. It may be equal to
extension. For example, a file app.js
will have a base extension and
extension of .js
dist
-"distribution": The destination directory.- Versioned file: A file with a file version, e.g.
app~fv=4mIbJJPq.min.js
. - Delimiter: The filever delimiter string is (by default)
~fv=
. The ending delimiter for a Version is any non-base64 character, such as another "~" character. This follows the standard URL Query and URL Fragment Query notation. - Non-dummy versioned files: a file with a digest as the version, e.g.
app~fv=4mIbJJPq.min.js
.
parent_directory
⎿ src
⎿ dist
Input src
directory structure is preserved in dist
. FileVer will version
files in src
, output to dist
, and perform Replace() in dist. FileVer by
default recreates the directory structure of src
into dist
. If this is not
desired, see examples for "manual" versioning.
Files that are not concerned with file versioning should be placed directly in
dist
.
Directories src
and dist
may be named as desired.
The suggested pipeline is to configure watch
to watch relevant source files.
- Use watchmod to watch specific files for change.
- On file change, configure
watch
to run a script that does (esbuild ->filever
).filever
is responsible for hashing the updated file, placing it into the dist directory, and update any source files references to the updated file indist
.
Then:
- modify js source file (file.js)
- watchmod is triggered, runs a
.sh
script that invokes 1. esbuild and then 2.filever
- esbuild minifies source file, outputs
src/file.min.js
filever
versions source file and outputsdist/file~fv=4mIbJJPq.min.js
(Version)filever
updates other source code files indist
with filever (Replace).
All text based source files that refer to versioned file should use the dummy
version in import references in the src
directory. After running, filever
updates references in dist
with versioning (input will be left untouched).
It is correct for Javascript files in src
to import use this form:
import * as test1 from './test_1~fv=00000000.js';
filever
Replace() updates imports in dist
to point to the correct file.
import * as test1 from './test_1~fv=4WYoW0MN.js';
src (Input Directory) Import |
dist (Output Directory) Import |
---|---|
app~fv=00000000.min.js |
app~fv=4mIbJJPq.min.js |
lib~fv=00000000.min.js |
lib~fv=820OsC4y.min.js |
Note: A consequence of this design is that the versioned files digest will not
match the current file name unless the references to versioned files are
re-zeroed. filever
's design assumes all Versioned file imports are zero'd.
If not wanting to use dummy files, each to-be-versioned file must be
individually enumerated. See Example_noDummy()
for a demonstration. Replace()
will still be needed to update references in dist
.
When contemplating the design, there were two designs considered:
- All files names had to be global unique or
- Use paths for namespacing.
The second option allows "duplicate" file names using path as a namespace, and is more Unix-link. There are many advantages to 2 over 1, so design 2 was implemented. This also allows the SAVR regex to be global, instead of needing to generate a SAVR for each subdirectory.
To implement 2 easily, all imports in dist
must be relative to directory
dist
.
For example, if in subdirectory named "subdir", imports must be relative to the root, e.g.
'../subdir/test_3~fv=CX_w_yNh.js';
and not
'./test_3~fv=00000000.js'
Given the files
test_3.js
subdir/test_3.js
Although these are easy to match in root, there is not good way to match a
secondary SAVR from subdir
. There is no good way to write a regex to match
the correct file.
'../test_3.js';
'./test_3.js';
Any regex constrain would put restrictions on the syntax of the language itself.
We want filever
to be syntax agnostic, other than Unix pathing which is widely
supported. Including relative root significantly simplifies.
The suggested version pipeline uses an input and output directory in order to
avoid update recursion. Since some formats, like Javascript modules, may refer
to other versioned files, this risks file version recursion. To avoid this, the
pipeline is one directional. It's suggested that source files that refer to
other versioned files use a dummy, constant file version in references, such as
app~fv=00000000.min.js
. Then, after the file version digest is calculated and
placed in the dist dir, the version in other source files is updated with
Replace(). This has the consequence that the versioned file's name, after
replace, will not be calculable from the source unless the dummy version is
restored.
See version_test.go
Because ETags require HTTP requests. Versioning precludes any HTTP request after the initial page load. That's far more scalable and efficient, especially considering the HTTP request itself is one of the most costly parts of page load.
DirHash: https://github.com/golang/mod/blob/ce943fd02449f621243c9ea6e64098e84752b92b/sumdb/dirhash/hash.go
filever
is released under The 3-Clause BSD License.
"Cyphr.me" is a trademark of Cypherpunk, LLC. The Cyphr.me logo is all rights reserved Cypherpunk, LLC and may not be used without permission.