Writing JavaScript without a build system
jvns.ca
jvns.ca
Lit gives you reactive components, declarative HTML templates, runtime encapsulated DOM and styles, interoperability with frameworks and HTML via web components, and a lot more - with no required build tools at all.
We take great care to make sure our libraries are usable without build tools. We support plain JS (in addition to TypeScript). Our libraries work straight from CDNs like Unpkg. When installed locally you can use a dev server that rewrites bare module specifiers or use an 8-line import map to make all of our core packages importable (we carefully keep all of our import specifiers compatible with the web and compact import maps). Components are inspectable with regular DevTools. You can even write components right in DevTools, or in a script tag in plain HTML.
I honestly wish this weren't a unique selling point of Lit, but as far as I can tell, of the "major" frameworks or component libraries out there (including React, Vue, Angular, Solid, Ember, Svelte, Stencil, Preact, and more) we're the only ones that fully work with plain JS within our regular mainstream usage patterns.
We were embedding the component into a legacy application and needed to use a build tool for older browser compatibility but it was a breeze to work with even with the extra complication our build setup added.
Getting back to basics and targeting what most modern browsers support out of the box is a huge plus.
vuejs can work without build system though. it's mentioned in the article, and I used to wwork that way. Eventually i made my own (minimal) build system though using quickjs and esbuild. Precompiling templates and minimising can be useful.
Is there any documentation about using it without a build? All the documentation and tutorials seem to assume a build step. I've found several different "getting started" entrypoints on the site, and they all begin with "npm install" and "import {LitElement, html} from 'lit';" which won't work without a build step, correct?
Regardless, we do have some workflows for completely "tool-less" (or local-tool-less).
- We publish bundles to jsDelivr: https://lit.dev/docs/getting-started/#use-bundles
- You can import directly from JS module CDNs like unpkg.com, ie:
import {LitElement, html} from 'https://unpkg.com/lit@2.6.1/index.js?module';
Ultimately we're not really dogmatic about things and support a wide variety of tools. The thing we care about is that tool-less development is possible, and that you can use standard semantics and tools with no special configuration (ie, regular Node resolution is all that's required locally, we stick to standard ES2020).>I generally don't consider installing from npm a build step. I also don't really consider rewriting import specifiers a build step - it's just locating files on disk.
Sorry, maybe this is just my JS ignorance, but I don't understand how this runs without a build step.
If I run npm -i lit, it puts lit into node_modules. But then if I put "import {LitElement, html} from 'lit';" into a JS file and run a dumb static webserver (like python3 -m http.server), what's linking "from 'lit'" to the necessary files in node_modules?
I just tried running the first tutorial[0] verbatim after npm installing lit to the same directory, and I get:
Uncaught TypeError: The specifier “lit” was a bare specifier, but was not remapped to anything. Relative module specifiers must start with “./”, “../” or “/”. my-element.js:1:31
I'd love to use this, I'm just not following how this works.Is it designed to run server-side rather than for static sites?
More info: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sc...
It seems like a handy way to make my code match the code in documentation that assumes a Node environment.
https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-a...
import '/home/user/project/node_modules/lit/index.js'
A relative specifier: import '../node_modules/lit/index.js'
Or a URL Specifier: import 'https://unpkg.com/lit?module'
So you have four-ish options to tell the browser where to look for 'lit':1. Use a server that will use the node module resolution algorithm and transforms it to a relative specifier as it encounters the import (transform/build-ish?)
2. Use experimental "import maps" to go fully buildless to tell the browser where to point that bare module to (buildless)
3. Bundle all your code (build step)
4. Use a bundle from a CDN
Here are some examples of each:
Server that transforms import specifiers (lit.dev does this by default):
https://lit.dev/playground/#gist=9092862a77acf73dd1e6faa73a3... (build-ish)
Import Maps (2):
https://lit.dev/playground/#gist=0c4f66396b333c82cb27e7dd3b2... (buildless)
https://lit.dev/playground/#gist=91c0b286698612b013eb81881ff... (build-ish)
Url Specifiers (2):
https://lit.dev/playground/#gist=05655736300f0425644e61a6264... (buildless)
https://lit.dev/playground/#gist=6d23e8e8dadacd7488bdec81d0f... (build-ish)
hope this helps with some of your questions
It does have lower level packages if you don't want ready made components too
FAST is kind of like a Lit-clone (though it patches global prototypes) plus a design system base.
There are lots of design systems built with Lit that you can chose from instead of us including our own: Shoelace, Adobe Spectrum, Redhat Patternfly, IBM Carbon, Material Web Components and more.
One thing I struggle with from time to time is that if I have a web component created in lit, and somewhere inside I have a dependency that has some CSS, that CSS doesn’t seem to work when the web component is consumed by JS.
The only workaround I found was to override the behaviour and instead of returning a shadow dom, return the component on its own, but that has its own downsides.
Jealous! All I ever see are react shops, angular shops that are now 'legacy', and the odd place that uses vue.
Lit is another library that is made up of a couple of things one of which is lit-html but is closer to a React or Vue by way of comparison.
Some noticeable differences between it and other view layer libraries include the fact that it was written by a team who are arguably much closer to actual browsers engineers and it uses native Web Components. The side-effect of this is that it’s incredibly fast and lightweight. I tend to think of it as the 5kb developer experience enhancement that takes Web Components as a primitive and makes them an actual pleasure to work with.
There are also a series of enhancement packages that are also totally opt in if you want more out of the box like internationalisation, client side routing etc.
If you go to lit.dev the documentation is actually pretty great and it’s probably the easiest way to get started.
But thank you for your response. Did not know that I was looking for Lit all these time. Every time I want to dabble into web dev, I get frustrated by multitude of tools need to setup first. All I wanted was to import a library using script tag and start building.
Vue (v2, don't know about v3) can also work without a build script, either with a render function, or a "template" (an string that mixes HTML with expressions).
We started using it because we knew it could be embedded into any other framework, we are using two others in the company. But are starting to use it for SPA also. Glad to see it growing quickly.
Thinking about possibly reworking our SPA to use Phoenix LiveView most of the abstractions are there already.
{
"compilerOptions": {
"checkJs": true,
"target": "es2020"
}
}The JavaScript community, in all its wisdom, reinvented edit-compile-debug loops for its immediate, dynamic language and I'm still assmad about it. So assmad that I, too, forgo all that shit when working on personal projects.
(The enterprise fucking loves React and Vue. Can't avoid it at work.)
There's probably merit in minification and compressing code though. But for the types of projects OP is talking about it's going to be single digit KB of JS anyways.
You can still do that. Nothing has changed. It's probably gotten even easier. Have fun implementing a complex UI without some sort of component Framework though.
You can have something like a `lib` directory in your project and place submodules for other projects in there. The dependencies need to provide some sort of a prebuilt artifact in the repo that you can import (or natively support ES Modules), but I've had good luck so far.
I'll also shout out jsdelivr[1] which is great for pulling dependencies directly from github.
1. Use nvm + an .nvmrc file in your project to pin the major version of Node.js you are using. I recently got a five year old Node 8 project up and running with no issues using this method.
2. Try to avoid packages that has binary dependencies (like node-sass). 99% of binary issues are fixed when using the correct Node version, the only other problem that can occur is if you switch architecture (eg. x86 => ARM).
Example:
{
"engines": {
"node": ">=0.10.3 <15"
}
}
Also, god forbid you are actually running anything using Node 8 and whatever dependencies you have there, that's a recipe for security disaster.The truth is that to use the NodeJS ecosystem we must have the discipline to keep projects up-to-date. I personally have a policy to update all my NodeJS projects at least yearly, even if they are abandoned, this includes all dependencies and the NodeJS version. Because I do it often most of the times there are not many problems to keep it working, and consequently updating is fast, but if I leave it for a few years and then come back I'll be in for a (bad) surprise.
[0]: https://docs.npmjs.com/cli/v9/configuring-npm/package-json?v...
edit: someone here in this thread suggested Volta [0] which seems literally like what I was looking for. No need for the nvmrc as it simply follows what's in the engine property of the package.json. Even better is that it's not a weird massive shell script but instead an actual program written in Rust. I'll have to try it out sometime.
[0]: https://volta.sh/
Even better, Volta automatically uses whatever node and package manager versions are declared in package.json -- no explicit call required. In other words, `node -v` and `npm -v` always return the version specified in package.json. It's been helpful for working with hundreds of repos and keeping CI and local dev synced on which versions to use automatically.
That's the main reason for me not to use a build system. You lose that "transparency" and accessibility of the code. With "raw" HTML/JS, the user can just copy paste your HTML/JS (often it was just 1 file for both combined!) into notepad, save as HTML and have their own website/"app"!
At least, that's how I felt until I got comfortable with TypeScript, and now I will refuse to use raw JS even for small projects... Alas! (Would be nice if V8 could strip type annotations and just run the plain JS hidden underneath... would also help with pasting TS snippets into the dev console!)
Some things can’t be done like this, like TypeScript, for which I keep it to a minimum, look for stability, and often include a full copy in revision control.
I'm hoping ES module tooling, import maps, etc. get us back to that point.
1. MDN has a comprehensive guide on JavaScript modules [0]
2. A build system free way to build interactive websites could be to combine libraries like htmx[1] and or lit[2] or just the sub package lit-html[3]. Or just go with native web components and a bit of AJAX.
[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guid...
[1] https://github.com/bigskysoftware/htmx
It leans into a lot of the web components spec so the library is pretty small, and the templating system is just ES6 tagged template literals parsed by the browser's DOM parser and any embedded variables are updated dynamically [1].
Try to use as few dependencies as possible.
You don't need a framework--components are achievable without vue and react etc.
Server side rendering and VDom are really not needed for most projects.
Think of Programming in JavaScript, CSS and F5 rather than Typescript and SASS as the equivalent of Marcus Aurelius' Stoicism.
Hahaha. You joke.
Import maps are also worth exploring. Documentation isn't great but I've managed to ditch npm on some side projects and use import maps and CDNs instead. Worth it for small projects without a doubt.
https://nginx-playground.wizardzines.com/
I recently had the chance to start a small greenfield project. As we have a lot of "tribal knowledge" of Vue I decided to keep it, but go with a "minimum viable tooling" approach. This means esbuild with no plugins, .tsx instead of .vue files (because esbuild natively supports TSX), no hot module reloading (just esbuild's simple refresh-the-page-on-build), and absolutely no CSS-in-JS, just a single style.css using BEM conventions.
The same approach would probably work with any other frontend framework that can use JSX or plain JS files, even if you do run it through esbuild before it gets to the browser.
In my experience, most of the complexity that creeps into JS projects is fancy build tools. Using fewer, better tools which focus on standards-compliant content is the way forward.
And I'm torn about styles. I've inlined some and done plain css for others. Though it's still just plain tsx to esbuild.
I've loved it. You can add timestamps to log output by piping a command through the ts Linux command. And I have never seen the second change (it doesn't do sub second timestamps) between the start of a rebuild and the rebuild being done. It's amazing.
I do this too, and no-build is definitely the way to go for them.
To make this even less effort, I log directly into the web server and edit them live. Once I'm happy I check in the changes.
The beauty of it was that it was a single static HTML file, copied verbatim to the archive. The only moving part, apart from the actual images/posts/videos, was a file called soup.js, which was just `window.soup=` plus the content metadata as JSON.
I didn’t even bother to have a separate JS file for the actual code, much less a build system. I put the JS right into a `<script>` tag in the HTML. I used ES modules, HyperApp as a minimalistic React/Redux workalike, and mini.css for some sane default styling.
I was amazed at how far I could get with such a rudimentary tooling. 117 SLOCs of HTML+JS, working equally well served from a server and from a local filesystem, complete with pagination, permalinks, and a jump-to-page dialog.
Here it is in action, serving a friend’s soup archive: https://soup.tomash.eu/archive (warning: some content can be touchy and/or NSFW). View source for a glimpse of how it works.
Are you just 1 person working on a project?
A lot of "best practices" aren't particularly needed when you're flying solo and know your code and mental processes inside and out.
Where this starts to get hairy is when you have multiple developers all trying to understand one person's mental model of how the code is structured, all in one file that they have to work in.
you can just include them in a page an go:
1. Typescript in VSCode.
2. Simple script to watch a project directory for changes to .ts files that runs...
3. ...'deno cache <file>' to transpile them to JS. Then copy them from the cache directory.
deno also has a bunch of options for formatting, bundling etc.
I guess “enum” as a special snowflake would fail this.
Where I’m coming from is that I can live without most build steps for a small project… but I can’t live without typescript.
We've also removed npm build systems from our newest .NET Project templates which uses JavaScript, Import Modules to progressively enhance Razor Pages Apps, which I've also written about in:
"Simple, Modern JavaScript"
https://vue-mjs.web-templates.io/blog/javascript
As you have access to full Vue 3 you're still highly productive being able to use Vue 3's Composition API and Plugins & Components which are able to be loaded directly in the browser.
We're still using TypeScript definitions for enabling static analysis benefits for external libraries but have switched to JSDoc type annotations for App code to avoid any runtime transpilation.
The fact that they added a breaking change to the way node runs, with no information in the error message about how to fix it, or even what to fix is unbelievable. We're still running node 16 just because some libraries don't work with the new SSL system.
It's a massive pain.
I don't need to write JS much, but when I do, it's with a browser open in one window and a text editor in another.
Anyways, I made a slightly more advanced buildless vue project here: https://github.com/kyleparisi/buildless-vuejs
It has the advantage of using .vue files which I enjoy. Oh and guess what… it has code splitting because you have to define what components the page needs ;).
Just leave out the attachShadow.
Me too. And the tips already in here are pretty good!
I know there's JSDoc but the normal syntax is so much better.
EDIT: Looks like there's a proposal: https://github.com/tc39/proposal-type-annotations
Typescript is baffling-ly complex to use compared to something like python.
What's complex about that?
The author even shouts out esbuild as being "a little more stable", which Vite uses under the hood.
But the core package is ~27k SLOC with upwards of 40 dependencies. Thats an indirect, but poignant statement illustrating how much work goes into solving just a tooling problem. And while it may hide some of the overhead in its own abstraction, which it does a fantastic job of, the overhead is still there and it can still break in arcane ways.
I think the spirit of the question of "why do we even need Vite in the first place?" is whats really being explored in the original post.
I certainly don't shy away from build tools at all, but I do often try to start projects without them just as an exercise to see if they really ever end up being needed. Especially when its so easy to drop-in something like Vite after the fact if its necessary and/or clearly adds an outsized return on investment.
In this case the A in YAGNI stands for “Are”.
The reason why it’s needed at all is it adapts npm packages that are not es modules. It gives you access to all of npm while requiring very little in return.
You can even turn off any transpilation/minification if you want to it to be more 1:1.
Instead of the increasingly silly attempts of coming up with weirder ways of doing server side rendering.
[0] https://reactjs.org/docs/add-react-to-a-website.html#quickly...
- make
- Google's closure compiler (standalone Jar)
- imagemagick
It builds fast and never goes out of date.
I did a talk about the topic of no build web apps recently. The slides are here for anyone interested: https://1drv.ms/p/s!ArEoTVF2ayv3lJg4oECZk2h5eN3I3Q
In preparation for that talk I’ve also adapted create react app’s default project into a no build template, including a variant with routing. That can be found in github: https://github.com/jsebrech/create-react-app-zero
Also I do not really use any frameworks. Just some libraries with particular functionality when needed.
<script type="module">
import Pkg from 'https://site.com/pkg.js'
import sheet from 'https://site.com/sheet.css' assert { type: 'css' };
</script>
For small personal projects I just use git submodules[1] and symlinks, and I still haven't regretted it.
Maybe it's because I set `submodule.recurse true` and `push.recurseSubmodules check`, but I still haven't found the reason why they get so much hate. I started using them (only on personal projects) partly to find out by myself what all that was about.
For $DAYJOB I obviously go with what's mainstream and "best practices", but on personal projects I try to experiment and challenge assumptions to see how much I can simplify stuff, and what decisions I come to regret 6 months down the line.
I either learn that some things are not as bad as others say; or I learn more about the specific pains caused by not following some advice (so I'll have more context on when it shouldn't be applied).
[1]: Pointing to my own mirrors. Because not owning your dependencies is a recipe for disaster.
The trade off is that because it’s actually reading JS from the zip files at runtime, a lot of Node packages don’t play nicely with it. Once you get things set up, it’s great, but getting there can be exhausting for a hobby project.
Imagine that: using your VCS/SCM to version control the code that goes into making your app work.
> Combined with a Node version file, you can be pretty sure that you’ll be executing the exact same code in a decade.
I have news for you: the Yarn-/NodeJS-specific stuff isn't necessary. With the code in your repo, you're able to roll back to a specific revision at any time. That's the whole point of version control.
And you _can_ manage a dependency tree and keep everything up to date by hand, but it's much nicer to let Yarn do it for you.
My setup for a nice ui dev experience now consists of 3 things:
- yarn plug-n-play
- esbuild, invoked from thr cli and not node, with no plugins. I run it in watch mode.
- caddy as the reverse proxy to serve the watch files and the api under a single url
It has been refreshing fast. And add into that generating TS api bindings for my backend server automatically... well it's great.
I've been developing web apps for the last few years and I really can't understand why all these are needed. They do not make my life easier as a developer, nor they make the code more readable.
I've asked around why do we need, for example, React, and never gotten a straight answer.
I don't see why you _have to_ use a framework for that. Have people forgotten to write simple code using basic JavaScript?
Yeah, Ive wondered that as well. Whenever I see I have to setup a complicated build system just to run a simple hello world example, I just go yuck and stop
wget https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/css]
Saving to: ‘tailwind.min.css’
tailwind.min.css [ <=> ] 2.80M 321KB/s in 10s
2023-02-17 18:44:58 (286 KB/s) - ‘tailwind.min.css’ saved [2934019]
In short: 2.8 megabytes, 10 seconds for the download.From the post:
> but Tailwind 3 doesn’t seem to be available as a big CSS file at all anymore […] so I’m going to keep using Tailwind 2 for now
Gee I wonder why. The length people go to make internet worse for everyone…
This site can’t be reached
5 years? More like 5 minutes in my experience. I probably spend 50% of my time trying to figure out why the build system is breaking, again.
In the Python ecosystem some of the tooling does not exist, because it is not needed. Usually Python code is not shipped over network each time a website is called. Hopefully people keep dependencies of projects to a minimum, avoiding to have to fix their mistakes later by shaking out stuff. Python has an OK-ish module system, not great, but OK, compared to how JS started out. There is no need for 6-10 standards competing and requiring different "build tools" to make one huge cluster f of uglified code out of all the code of an application. Mind, it is 2023 and we still have no good way to tell the TypeScript compiler to simply spit out JavaScript code, that can immediately be served on a website, if the TS code has multiple modules. The JS ecosystem still suffers a lot from the not well thought through basis of the language and historical burden of that.
Python is far from perfect itself of course. Plenty of problems in its ecosystem as well.
This is not my experience of Python. Some of the tools I’ve installed via Homebrew have whole forests of dependencies. And don’t get me started on all the different Python versions they require.
I agree with your sentiment entirely about "When [...] it has become too much, one should really think about not getting that code in there in the first place," but that's really a problem with the NPM community in particular—not something that people who aren't NPM programmers can do anything about (and who, as people who work with JS, are even more annoyed by it than people hailing from communities that use other languages).
> Mind, it is 2023 and we still have no good way to tell the TypeScript compiler to simply spit out JavaScript code, that can immediately be served on a website
I guess it's actually necessary to point out the obvious here: TypeScript is not JS. The fact that TypeScript superficially resembles JS does not make the sins of the TypeScript team JS's responsibility. You could swap "TypeScript" for, say, FartTwist, an imaginary programming language that I just made up and doesn't resemble JS (or anything else that transpiles to JS with tools that have the same problem the TypeScript ones have), and the criticism would be exactly as applicable.
The big problem with the JS ecosystem is that it's filled with people who clearly don't like programming in JS, and yet they advertise themselves as part of that milieu. In fact, enough of them have banded together that they've managed to almost completely commandeer JS's public image, to the point that when JS is mentioned what comes to mind are their shenanigans, inevitably leading to discussions like this one.
In my own experience though, python is far from batteries included. Ill tackle one vertical: packaging & dependency management. Historically you didn't get much out of the box for this problem.
You have pip, virtualenv, virtualenvwrapper, pyenv, pyvenv, pyenv-virtualenv, pipenv, et al. Almost a dozen different, and sometimes redundant, solutions for a problem that historically python didn't solve natively for free. Poetry has drastically improved this problem, but not enough to really distinguish python from JS in a meaningful way IMO.
Python projects (in my personal experience) require jumping through a lot of hoops to get dependencies working correctly.
It could be my experience with Python is just bad luck!
My experience with dependency-"free" web python is limited to small projects using Bottle (one downloaded .py file) and sqlite for tiny sites.
And a Tcl "starkit" aaages ago.
Deno is a single portable binary file that can be embedded in app bundles and invoked on users' machines. That's easier said than done with Python.
Build tools for JS solve problems that other languages don't have to worry about. You can't use Python (directly) in a browser which means none of these things are important:
1. Bundle size. This doesn't really matter when you're just running it locally. Maybe you want the final binary to be somewhat small, but it could still be many dozens of megabytes and it'd be a non-issue. If a website was like 50MB, it'd be incredibly slow.
2. Platform compatibility. Every user is running a slightly different environment. Gotta support different browsers, or older ones? Now you need to polyfill the language features to exactly what the browser supports. Compiled languages don't have to worry about that at all, and even with Python, you're mostly just worried about the major language version a user might have installed (iirc).
3. Anything related to writing UIs. In JS, you're writing for the browser, which has a pretty limited set of APIs for writing complex, interactive UIs. Hardly anything to help write interactive declarative or reactive UIs. On top of that, since the site is always remote, pretty much everything has a round-trip to a server involved. This means you may start pulling in dependencies to help solve these problems. In Python, you probably just choose a UI framework like QT and it gives you most everything out of the box.
My point is just that we tend to compare JavaScript to other languages without really considering the unique environment JS gets deployed to. It's really not comparable. If Python (or any other language) was running in a browser, you'd have a huge new class of problems for that language to solve which it just didn't need to care about before.
I feel like the meaningful comparisons to JS are related to language syntax, type system, local runtime performance, dependency management, etc. And those topics are a lot more subjective!
Explain that to all the people writing browser extensions which by their very nature have an entirely different set of relevant engineering concerns in contrast to Web apps delivered by HTTP—and yet the same minification tools and compatibility shims still abound for as far as the eye can see.
Be mindful of your blindspots.