Source file tree builder.
setup a lsp ( livescript + stylus + pug ) watcher:
require! <[@plotdb/srcbuild]>
srcbuild.lsp {base: 'web', i18n: ..., logger: ...}
where
base: root dir forsrcandstaticfolders. default.i18n: i18n object.ignored: files to be ignored. in anymatch-compatible definition.- by default ['.git']
hash: optional. content addressing for built files. off unlessenabled. see Content Addressing.logger: optional. for logging output. useconsole.logby default.-
sample logger with
pino:require! <[@plotdb/srcbuild pino]> srcbuild.lsp({logger: pino({level: 'debug'})})
-
These fields will be passed to all customized builders. Additionally, configurations in builder-specific fields will lso be passed to corresponding customized builders. For example, bundle field will be passed to bundle builder:
srcbuild.lsp {bundle: { ... /* this will be passed to bundle builder */ }, ...}
For lsp, there are 6 different builders:
lsc: build*.lsfromsrc/lstostatic/js.stylus: build*.stylfromsrc/styltostatic/css.pug: build*.pugfromsrc/pugtostatic.bundle: bundlecssandjsfilesasset: copy whitelisted extensions fromsrc/assetstostatic/assets.raw: copysrc/rawtostatic, verbatim. see below.
See following sections for additional options in custom builders.
Everything above generates its output. A site also has files that are simply served:
favicon.ico, robots.txt, images, fonts, a site.webmanifest. Put them in src/raw
and they land in static unchanged:
src/raw/favicon.ico -> static/favicon.ico
src/raw/robots.txt -> static/robots.txt
src/raw/assets/img/logo.png -> static/assets/img/logo.png
No extension whitelist - the tree exists to be copied, so filtering it could only mean
silently failing to ship a file someone added. Junk is still excluded ( .DS_Store,
Thumbs.db, *.swp, *~, .git ), and anything in ignored on top of that.
srcbuild.lsp {raw: {srcdir: 'src/raw', desdir: 'static'}} # the defaults
srcbuild.lsp {raw: false} # turn it off
Why this is worth doing. It is what makes static/ entirely derived. Once no file
exists only there, rm -rf static is always safe, the directory does not belong in
version control, and a deploy is a build rather than a merge of hand-placed files with
generated ones.
raw is a separate option from asset, deliberately. asset is the older
whitelist-based copier ( src/assets/**.{png,gif,jpg,svg,json} -> static/assets ), and
projects override it - servebase points it at src/pug so images can sit next to the
pug that uses them. If raw were another entry in asset, every one of those overrides
would silently drop it. Both run; migrate at your own pace.
A generated file keeps its name and changes its bytes on every build, so its url cannot be cached: the browser has to ask every time whether it is still current. Content addressing gives it a second name derived from what is inside it, which can be cached forever because that name can never mean anything else.
Off by default. It rewrites the url of every generated asset in every page, and buys
nothing until the server in front actually serves the addressed form with a long
max-age, so a project turns it on once it has done that:
srcbuild.lsp {
hash:
enabled: true # off unless set
mode: 'filename' # or 'query'
keep: 3 # filename mode: generations kept
keepDays: 0 # filename mode: also keep anything younger than this
}
Two modes:
filename also write `<name>.<hash>[.min].<ext>`; pages point at that.
a url names exactly one byte sequence, so it can be immutable. old
copies have to be expired, and html older than the retention window
points at a name that is gone.
query leave one file and point at `<name>.min.js?v=<hash>`. nothing
accumulates and nothing 404s, but html older than the last build
silently gets whatever the file holds now, and some CDNs ignore the
query string when caching.
Either way the plain name is always written and always current. It is what already
deployed html points at, what a page rendered before the first build falls back to, and
what a try_files in the server can fall back to in filename mode.
Covers what lsc, stylus and bundle produce - reached through the script and
css mixins and the bundle filter. A url written directly into a template, an image,
or anything not built here is passed through untouched.
<base>/.bundle-dep/manifest.json, one per base, shared by every builder:
"/js/site.min.js": {
"url": "/js/site.4b6ac41e1bea.min.js",
"refs": ["src/pug/index.pug"],
"generations": [{"files": ["static/js/site.4b6ac41e1bea.min.js"], "at": ...}]
}
url is what the mixins look up - pug cannot compute it, since it never reads the
built file. refs is which pug files embedded the url, and is the only way back to
them when the hash moves: a built asset is in no page's pug dependency graph, so
nothing else can know a page went stale. generations is what lets old copies be
expired.
It is an index into static/, so the two belong together. Losing it is recoverable but
not free: url comes back on the next build ( existing outputs are adopted ), while
refs only comes back when pages actually render.
A generation is deleted only once it is both beyond keep and older than keepDays.
Count alone answers the wrong question - three rebuilds can be three hours or three
months, while the risk is how long a browser tab stays open. keepDays defaults to 0,
because a client holding old js across a deploy is already exposed to backend api
drift, and the answer to that is a "site updated, please reload" prompt rather than
keeping every artefact forever. Raise it if you would rather spend disk.
There is no sweep: expiry happens when that url is next rebuilt. So nothing grows without bound, but a url that never changes again keeps whatever it had.
lsp returns the watcher; watcher.ready is a promise that resolves once every
adapter's initial scan has built, including the bundles those builds triggered.
const srcbuild = require('@plotdb/srcbuild').lsp({base: 'web'});
await srcbuild.ready;
app.listen(port);Without it a host starts serving during the first build, which is the heaviest build of the process's life. That is where cold-start flakiness comes from: on makechart, every one of 38 database connection timeouts over four years fell within 30s of a build event, 28 of them within 30s of a start, and none at all in the 30-120s band.
It never rejects. A source that fails to build has already logged; refusing to start over one bad file would be worse than serving the rest.
Bundles are waited for separately from the adapters, because a bundle is not built by
the watcher noticing a file - it is built because a pug page named it through the
bundle filter, one tick after that page's own build resolved.
Minification runs on a worker_threads worker, not on the main thread.
It matters when srcbuild shares a process with a server, which is the usual dev setup.
uglify-js is synchronous CPU work and a large bundle takes seconds: measured on a
0.94MB bundle, 2677ms of minify blocked the event loop for 1769ms in one stall. Nothing
else in that process runs during it - long enough for a fresh pg.Pool connect with a
2s timeout to expire while its handshake callbacks cannot be delivered, so the request
fails with a database error that has nothing to do with the database.
The same bundle through the worker: ~20% more total time ( a worker has its own heap and warms its own JIT ), 9ms of it spent moving the strings across, and the loop's worst tick was 13ms.
There is no size threshold, because cost does not track size: in the same corpus 800KB took 88ms and 960KB took 2319ms, one construct in the last chunk being pathological for uglify. Everything the builders minify goes across.
The worker is spawned on first use, unref'd, and terminated after 30s idle. If it
cannot start or it dies, minification falls back in-process for the rest of the run -
slower, never broken.
SRCBUILD_MINIFY_WORKER=0 keeps everything in-process.
Two things stay synchronous, both deliberately:
- the
lscandstyluspug filters. Pug's filter interface has no async form. They handle inlineinclude:lscsnippets, which are small. - a source file that ships its own
.mintwin is never minified at all, so it never reaches the worker.
On failure the minifier returns the input unchanged and logs. It never writes an empty
output: uglify-js.minify signals a syntax error by returning {error} with no code
field, and reading .code off that used to yield an empty .min.js, or - inside a
bundle's join - a file that silently vanished from the output.
While a bundle is being built, further requests for that same bundle do not queue. They set a flag, and the run in flight does exactly one more pass when it finishes - which reads whatever is on disk by then, so it subsumes every request that arrived while it was busy. N requests cost at most two builds.
This matters because rebuilds arrive in bursts: fedep touching every lib file, or a
save that invalidates a shared include. Before this, makechart's log shows one bundle
built back to back at 8.6s, 6.4s and 3.4s. Moving minification to a worker does not help
there - it only moves the queue onto the other thread.
force is sticky across the collapse: if any collapsed request needed the freshness
guard bypassed ( because the source list changed, which mtimes cannot show ), the
rerun bypasses it too.
Extend base builder for a customized builder:
base = require("@plotdb/srcbuild").base
mybuild = (opt = {}) -> @init({srcdir: 'src', desdir: 'des'} <<< opt)
mybuild.prototype = Object.create(base.prototype) <<< {
is-supported: (file) -> return true
get-dependencies: (file) -> return []
build: (files) -> # build will be called if is supported.
}
with following user-defined functions:
is-supported(file): return true iffileis supported by this builder, otherwise return false.file: file name for file to be verified. Relative to cwd.
get-dependencies(file): return a list of files that this file depends on.file: same asis-supported
build(files): should compile / generate target files of given file listfiles.files: a list of objects corresponding to files to be compiled, with following fields:file: path of the file to be built, relative to cwd.mtime: timestamp of the modified time of this file. may be modified time of its dependencies.
purge(files): should remove generated files corresponding to files listed infiles.files: same asbuild.
resolve(file): return source file path for given target filefile.- return null if the given target file can't be derived from any supported source files.
and the common options for init are as following:
base: root directory for srcbuild to run.srcdir: directory for source files. should be relative tobase. defaultsrcif omitted.desdir: directory for built files. should be relative tobase. defaultstaticif omitted.logger: logger for log output. useconsoleif omitted.initScan: default true. if true, run a directory scanning for files to build when adapter is initing.
check src/ext/lsc.ls or src/ext/pug.ls for example.
Except common options, each builder may support different options:
pug:-
intlbase: base dir to place i18n files. for example,intlpart of/intl/zh-TW/index.html. defaultintl. -
i18n: an optional i18n object having the same interface withi18next- when provided, enable i18n building with following additional features:
- if
buildIntlis set to true, build source files to locations by i18n config like/intl/zh-TW/index.html. - an additional function
i18nwill be available during pug compilation.-
i18n(text): translatetextbased on thei18nobject provided. -
language(): return current language. ( e.g.,zh-TW) -
intlbase(p, lng): return a path to givenp, based on current i18n ( or specifiedlngarg ) setup.- for example,
intlbase('link', 'kr')may generate/intl/kr/link, based on he base dir config.
- for example,
-
additionally, a pug filter
i18nis also available, which can be used like:span:i18n translate this text
-
- if
- when provided, enable i18n building with following additional features:
-
noView: default false. when true, js view files ( generated toviewdir) won't be built. -
buildIntl: default true. when true build locale-based files understatic/intl/and.view/intl- requires
i18n; omitted ifi18nis not available
- requires
-
viewdir: default.view. a directory for storing prebuilt pug files ( in .js format ) -
bundler: default null. Auto packing will be possible only if this is provided. -
locals: additional local variables for pug context when compiling.
-
lsc:useGlslify: default false. set to true if you need glslify of lsc files.- NOTE this is an experiment feature and may be removed ( move to standalone builder ) in the future.
bundle: bundle options. includes:configFile: json file storing bundle configuration. optional.relativePath: use relative path for paths in config file. default false. possible values:false: all files inconfigFileare relative to current working directory.true: all files inconfigFileare relative to the directory containingbundle.json- or, specific a path as the relative root.
manager: block manager, optional. required for @plotdb/block bundling.- can be either an
block.managerobject, or ... - a function returning such object which accepts an object with following fields as parameter:
base: the base dir of this bundle.
- can be either an
config: bundle configuration in following format: { "css": { "name": [ ... list of files to bundle together ] }, "js": { ... } }
asset: for copying asset files.ext: array of file extensions to copy. default["png", "gif", "jpg", "svg", "json"]
These options are constructor options for corresponding builder, e.g., for pug builder:
new pugbuild({ i18n: ... })
When using shorthands like srcbuild.lsp(...), you can also specify corresponding option in scope, such as:
srcbuild.lsp({
base: '...', i18n: '...',
pug: {intlbase: '...'}
});
common options will be overwritten by scoped options.
Send adapters to watcher from getAdapter() of each custom builders:
require! <[@plotdb/srcbuild/dist/watch @plotdb/srcbuild/dist/ext/pug]>
pugbuilder = new pug(...)
watcher = new watch({adapters: [pugbuilder.getAdapter]})
By default, watcher watches the current working directory. Change watcher behavior with following constructor options:
adapters: array of adapters to use to handle file change events.ignored: array of glob strings to ignore when watching for changes. by default[".git"].root: directory, or array of directories to watch. by default["."].logger: optional. logger object with logging functions such asinfo,warnanderror.
use watch.demand(target-file) to force rebuild by request. e.g.,
require! <[srcbuild]>
watch = srcbuild.lsp!
# this triggers rebuilding of `web/src/pug/index.pug` file.
watch.demand('web/static/index.html').then -> console.log "built."
target to source file mapping is done by resolve function in custom builder, so to use on demand build, resolve must be implemented.
use srcbuild.i18n to quickly setup an i18next object:
require! <[srcbuild]>
srcbuild.i18n(options)
.then (i18n) -> srcbuild.lsp {i18n}
options is passed to i18next init function. Additional fields in options used by srcbuild.i18n:
enabled: true if i18n is enabled. default true
When i18n object is provided, i18n data can be used in pug files via i18n function. e.g.,
div= i18n("my-key")
will show my-key content defined in locale corresponding default.yaml:
my-key: 這是我的鍵
To use a namespaced key, add : before key. For example:
div= i18n("new-ns:another-key")
will access to another-key in new-ns.yaml. Be sure to add your namespace name in ns field of i18n option:
"i18n": { ... "ns": ["default", "new-ns"] }
additionally, use intlbase to wrap path with a i18n based relative path:
a(href=intlbase('/faq'))
When building, we extend Pug via plugins and filters to support more features.
Use @ to include files in modules:
include @/ldview/dist/ldview.pug
Use @static to include files under static folder:
include @static/assets/sample.pug
Other paths starting with @ are reserved and will cause error when used.
use script and css builtin mixins to load external script and css files:
+script({name: "module-name", version: "main", path: "somefile.js"})
+css({name: "module-name", version: "main", path: "somefile.js"})
where the fields of the parameters:
name: module nameversion: module version. defaultmain, if omitted.path: path of file to load. defaultindex.min.js, if omitted.defer: defer execution or not. defaulttrueif omitted.async: async loading or not. defaultfalseif omitted.
By default the above script mixin generates a script tag pointing to files under /assets/lib/<name>/<version>/<path>. You can customize the /assets/lib/ by calling libLoader.root(desiredPath).
With content addressing enabled, a url these mixins emit is
looked up in the manifest and replaced by its addressed form when there is one. A url
with no entry - an external url, a file this build did not produce, anything before its
first build - is emitted unchanged, with libLoader._v appended as before.
Additionally, you can also use a list of modules:
+script([
{name: "module-a", version: "0.0.1", path: "somefile.js"},
{name: "module-b", version: "0.2.1", path: "another.js"},
{name: "module-c", path: "with-default-version.js"},
{name: "module-d", version: "with.default.path" },
{name: "with-defer-async", defer: false, async: true}
{name: "omit-everything"},
])
Use the second option object to specify additional parameters, including:
pack: experimental deprecated default false. Enable auto packing or not.- if true, enable auto packing which trigger bundling automatically to a filename from md5 of all script urls.
- require
bundleroption in pugbuild constructor. - doesn't work with external urls.
- there are still issues about rebuilding and build from view.
- replaced by
bundlefilter, which runs in compile time.
min: default true. When true, use minimized packed file with pack option.
Following formats and filters are supported:
-
lsc: transpile content from livescript to JavaScript. -
stylus: transpile content fromstylustoCSS. -
md: transpile content frommarkdowntoHTML. -
bundle: bundle files including js, css or block. usage sample::bundle(options = {type: "block", files: [ { bid }, ... ]})
Following functions are added:
md(code): convertmarkdowntoHTML.yaml(path): readyamlfile and return object. (tentative)yamls(path): read content ofyamlfiles underpathdirectory. (tentative)asseturl(url, src): the content-addressed form of a built file's url, orurlunchanged when there is none.srcis the pug file asking, recorded so the page can be re-rendered when the hash moves. used by thescriptandcssmixins.bundleurl({type, name, min, src}): the same lookup for a bundle, addressed by its spec rather than its url. returns null when the bundle has not been built yet, so callers fall back to the plain name.hashfile({type, name, files, src}): declare a bundle from a list of files. used by thepackoption of the mixins.
asseturl and bundleurl do nothing but return their input when content addressing is
off, so a template can call them unconditionally.
There are some additional i18n filters available if properly configured. See above for more information.
MIT