Typescript for Web Pages, Simplified

A few days ago, I did the stupid. I accidentally wiped over some key PHP files from my personal home page's Concrete5 setup, and attempts to recover them were unsuccessful (yeah, stupid me forgetting where his backups were).

So having decided to start over, I had to decide 'with what'? Usually I use opportunities like this to pick up a new thing I'd never worked with before. My site is on a php-based host (cpanel+scriptaculous), so some of the more modern node options like ghost were out, as were anything involving docker. But in thinking about it, I realized I didn't need all of the weight that comes with many php-based CMS/Blog systems like wordpress, b2evolution, and concrete (in addition there's the matter that increasingly the 'themes' aren't free).

What did I really want? Easy theming, a list of my social media sites, a list to my other home pages, a place for a bio blurb, and spots where I could put the summary of the most recent article from each of my blogs/rss feeds. I also wanted a general-posting blog but I could externalize that separate from the personal home page itself. I don't need a static site generator or big CMS system for something like that.

But I might want to add a few special features through simple web-components, and the theming framework may need some coding. And doing that, I absolutely wanted Typescript. But again, I don't want a big rollup/webpack framework. I just want the deployed site nice and simple, using CDNs as much as possible.

So here's how I set that up and worked past a few odds and ends.

Initial Setup

First, init the project with npm/pnpm/yarn (your choice) and git (optional, but even if you don't store it in a repo, it can be useful for preservation, branching for experiments, etc). I'm not going to go into these as there's plenty of documentation out there.

Next, install typescript from npmjs. Also install serve so we have a web host to test from (some features we'll need won't work in file:// hosting - if you are coding within an apache or nginx setup, then you won't need this).

Now set up the hierarchy. The expectation is that the deployed page should just be the index.html file, the css folder, the js folder. That's it at first. Our dev layer will also have the ts folder for the script sources. So we create index.html, the ts folder with 1 test script, and the css folder with a single css file.

The typescript test.ts file is really simple:
const x: number = 5; console.log(x);

And the html file:
<html>
<head>
<script src="js/test.js"></script>
</head>
<body>
<article>hi mom</article>
</body>
</html>


To transpile the typescript to js, tsc is simple enough, but best to have a tsconfig file to make it easier to handle all the options. (This file will change as we go, so don't worry that it isn't doing things 'perfect' for modules just yet.)
{
"compilerOptions": {
"target": "ES2022",
"outDir": "js",
"declaration": true
},
"include": ["ts/*"]
}


Then we just add the start script into package.json, "start": "rm -rf js && tsc && serve ." and we'll be able to see our script run in the console of localhost:3000. This alone is potentially enough to get going - the typescript is transpiled, can be tested, and with the following package.json script, "dist": "rm -rf js dist && tsc && mkdir dist && cp -r index.html css js dist", we have a dist folder to copy to where we are going to deploy. While testing, after a change in the typescript, just tsc and reload the browser.

Modularizing

I, however, knew I was going to be making some simple web-components, and would rather have a single 'index.js' that imports everything. I don't need 'tree-shaking', i just didn't want my html file to carry a ton of <script> tags, when the ES6 module system is designed to handle that in modern browsers. Traditionally, we would need a webpack/rollup (and babel) setup for all this, with backwards-compatibility back to IE 11 and all that ancient history from the browser wars.

Create ts/index.ts file and add to it export ∗ from './test';

Change index.html to load js/index.js instead of js/test.js, and add type="module" to the script tag.

tsc and reload...and oops? A 404 happens because "./test" is not there in the js folder. Only "./test.js".

In a node build environment this is normally not a problem, because it all gets put into a single file. But here in this client-only situation, the browser is more picky and looks at the file name as an absolute when constructing the URL. Many have proposed that tsc add a configuration that would convert the imports in such a situation to the right suffix, but for various reasons that to be honest, read as a bit elitist, they refuse to build it in. So this leaves 2 options (well, the 3rd is to give up and webpack, but never mind).
  1. Change the compile settings to produce files without a suffix
  2. Get some script that would parse through the imports and update them to .js
I preferred the latter, as I'm a suffix hound when it comes to my local file system. Fortunately, I didn't need to write that myself, as an npm package exists to already handle that, fix-esm-import-path.

So now instead of calling tsc directly, we would build by a new script line in package.json, "build": "tsc && fix-esm-import-path --process-import-type js", and change the dist to call the build task instead of tsc directly (this will vary depending on your choice of package manager).

Now the resultant index.js file will import './test.js' and the browser will be happy. You can also see that the types declarations are there, and can verify that by changing index.ts to be
import { x } from './test';
console.log('x again', x);
and see in your editor that x is present, and a number not an 'any'.

Types and CDNs

Now I was starting to style the page and needed to script something from the 3rd party library I was using for the theming, beercss. This is a pretty impressive Material-You (aka Material Design 3) implementation that was useful because it had and has many features that Google had not yet released in their own web implementation, particularly dialogs/drawers, and works on styling existing standard dom elements instead of being all web-components. One feature it has built in is that you can feed it an image url and it will create a Material-You theme, dark or light, to the primary colors of the image, using material-design-colors.

Now back at the deployment choice. I could go back and put in the webpack/rollup and deploy my script as a huge bundle, but I didn't and don't want to do that for this type of page. I'd rather just load up the package via optimized for deliver/caching CDNs. So how do I get my use of their code type-safe?

Turns out you will need to locally install it. So first, following the CDN installation instructions, add the css and the 2 scripts (at this point, I moved the script tags to the bottom just before </body>). Then install it from npmjs with the package manager. We'll have it locally but it won't be part of our deployment, we just need the types.

But I just want the types...I don't want the code to get included in an import because the the browser will attempt to import 'beercss' and not find it (when we really have module imports from the web, this would be a different thing, but I'm not sure module imports from CDNs preserve types).

Turns out there's a syntax to import the types without importing the code that goes with it.

/// <reference types="beercss" />

With this, the exported types are loaded as if you did an import of all of them, and code like the following is completely type-safe without 'any'.
const themeImage = document.getElementById('theme-image') as HTMLImageElement;
const src = themeImage.src;
await ui('mode', 'dark');
await ui('theme', src);
.

Some times typescript will complain (found this out with showdown, which I'll talk about on the next entry about web-components) about the module type and want to force an import anyways. You can resolve that with /** @ts-expect-error */ above the line where it first complains.

At this point, I also did some changes to tsconfig to ensure the module code was working and producing up to date output.{
"compilerOptions": {
"target": "ES2022",
"outDir": "js",
"declaration": true,
"moduleResolution": "node",
"moduleSuffixes": [".ts", ""],
"esModuleInterop": true,
"module": "ES2022",
"lib": ["es2022", "dom"],
"typeRoots": ["node_modules/types"]
},
"include": ["ts/*"],
"exclude": ["node_modules/**"]
}
.

As I said, the next post will go over making a simple web-component that can import markdown content from another file and produce html.

Trackbacks

Trackback specific URI for this entry

Comments

Display comments as Linear | Threaded

No comments

Add Comment

Standard emoticons like :-) and ;-) are converted to images.
Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA