application-typescript 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # `application-typescript`
2
+
3
+ Run TypeScript in the browser without a bundler — add one `<script>` tag and serve static files.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ **`index.html`**
10
+
11
+ ```html
12
+ <!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8" />
16
+ <title>Hello</title>
17
+ </head>
18
+ <body>
19
+
20
+ <!-- app container -->
21
+ <div id="app"></div>
22
+
23
+
24
+ <!-- preact + htm application -->
25
+ <script type="application/typescript" src="./hello.ts"></script>
26
+
27
+
28
+ <!-- esm.sh application-typescript runner -->
29
+ <script type="module" src="https://esm.sh/application-typescript"></script>
30
+
31
+ </body>
32
+ </html>
33
+ ```
34
+
35
+ **`hello.ts`**
36
+
37
+ ```typescript
38
+ import { html, render } from "htm/preact/standalone";
39
+ import { HelloApp } from "./hello.app";
40
+
41
+ const root = document.getElementById("app");
42
+ if (root) render(html`<${HelloApp} />`, root);
43
+ ```
44
+
45
+ **`hello.app.ts`**
46
+
47
+ ```typescript
48
+ import { html } from "htm/preact/standalone";
49
+
50
+ export function HelloApp() {
51
+ return html`<h1>Hello World</h1>`;
52
+ }
53
+ ```
54
+
55
+ Serve over HTTP (not `file://`), then open the page:
56
+
57
+ ```bash
58
+ npx -y serve . -l 5123 --no-port-switching # → http://localhost:5123/
59
+ ```
60
+
61
+ That's it. No build step, no config file.
62
+
63
+ ---
64
+
65
+ ## When to use
66
+
67
+ Good fit:
68
+ - Small UI prototypes with 10–15 components
69
+ - Static pages where you want real TypeScript without wiring a bundler
70
+ - Exploring an idea quickly before committing to Vite / esbuild
71
+
72
+ Not a good fit:
73
+ - Production apps (CDN dependency, no tree-shaking, no SSR)
74
+ - Large codebases (compile-on-demand cost adds up)
75
+ - Environments that block CDN traffic (esm.sh required for the compiler)
76
+
77
+ When you outgrow this, move the same `.ts` sources into Vite or esbuild — the module structure carries over.
78
+
79
+ ---
80
+
81
+ ## Multi-file projects
82
+
83
+ Use relative imports between `.ts` files. The runner transpiles each file and rewrites `./` / `../` specifiers to blob URLs, so the full module graph loads correctly.
84
+
85
+ **`hello.ts`** (entry)
86
+
87
+ ```typescript
88
+ import { html, render } from "htm/preact/standalone";
89
+ import { HelloApp } from "./hello.app";
90
+
91
+ const root = document.getElementById("app");
92
+ if (root) render(html`<${HelloApp} />`, root);
93
+ ```
94
+
95
+ **`hello.app.ts`** (component)
96
+
97
+ ```typescript
98
+ import { html } from "htm/preact/standalone";
99
+
100
+ export function HelloApp() {
101
+ return html`<h1>Hello World</h1>`;
102
+ }
103
+ ```
104
+
105
+ Keep imports acyclic.
106
+
107
+ ---
108
+
109
+ ## Setup reference
110
+
111
+ ### Entry marker
112
+
113
+ ```html
114
+ <script type="application/typescript" src="./src/app.ts"></script>
115
+ ```
116
+
117
+ - `src` is required. Scripts without `src` are ignored.
118
+ - `src` is resolved with `new URL(src, location.href)`. Use `./src/app.ts` under a subpath, `/src/app.ts` at the origin root.
119
+ - Multiple markers are processed in document order, one after another.
120
+
121
+ ### Runner
122
+
123
+ ```html
124
+ <script type="module" src="https://esm.sh/application-typescript"></script>
125
+ ```
126
+
127
+ Must come **after** all `application/typescript` markers so the DOM query sees them.
128
+
129
+ ### Import map (optional)
130
+
131
+ You can skip the import map entirely — bare specifiers like `"htm/preact/standalone"` fall back to `https://esm.sh/<specifier>` automatically.
132
+
133
+ Add an import map when you need to pin versions or use non-esm.sh URLs:
134
+
135
+ ```html
136
+ <script type="importmap">
137
+ {
138
+ "imports": {
139
+ "preact": "https://esm.sh/preact@10.24.3",
140
+ "htm/preact/standalone": "https://esm.sh/htm@3.1.1/preact/standalone"
141
+ }
142
+ }
143
+ </script>
144
+ ```
145
+
146
+ The runner merges all import maps on the page (later maps override keys). `scopes` are ignored.
147
+
148
+ ### Serving
149
+
150
+ Use a static HTTP server. `file://` breaks `import`, `fetch`, and CORS.
151
+
152
+ ```bash
153
+ npx -y serve . -l 5123 --no-port-switching
154
+ ```
155
+
156
+ ### Smoke test (bare imports)
157
+
158
+ This repo includes `test.html`, `test.ts`, and `test.app.ts`. `test.app.ts` imports `lodash/shuffle` so you can confirm bare specifiers resolve through esm.sh in the browser.
159
+
160
+ ```bash
161
+ npm test
162
+ ```
163
+
164
+ That runs `npx -y serve .` (default port, usually 3000, with automatic fallback). Open `/test.html` and check the page text and console.
165
+
166
+ ---
167
+
168
+ ## How it works
169
+
170
+ For each `<script type="application/typescript" src="…">`, the runner:
171
+
172
+ 1. Resolves `src` against `location.href`
173
+ 2. Fetches the source (`cache: "no-cache"`)
174
+ 3. Transpiles with `typescript.transpileModule` (target ES2022, module ESNext, no type-checking)
175
+ 4. Rewrites relative imports (`./`, `../`) to blob URLs of the transpiled dependency files (recursive, cached)
176
+ 5. Rewrites bare imports to absolute URLs: import map match first, otherwise `https://esm.sh/<specifier>`
177
+ 6. Creates a `Blob`, calls `URL.createObjectURL`, and `import()`s the result as a real ES module
178
+
179
+ Each source URL is cached to one blob URL. Parallel loads of the same module share one in-flight promise.
180
+
181
+ The TypeScript compiler itself is loaded from `https://esm.sh/typescript@6.0.3` on first run.
182
+
183
+ ---
184
+
185
+ ## Limitations
186
+
187
+ - **Type-checking** — none at runtime; use `tsc --noEmit` / your IDE
188
+ - **Inline `<script>` bodies** — not supported; `src` attribute is required
189
+ - **Dynamic `import()` with non-literal specifier** — not rewritten
190
+ - **Import map `scopes`** — ignored
191
+ - **Compiler version** — pinned to `typescript@6.0.3` in the runner source
192
+ - **CDN dependency** — requires network access to esm.sh on first load
193
+
194
+ ---
195
+
196
+ ## Security
197
+
198
+ Treat every fetched `.ts` URL as executable code. Only point `src` at same-origin files or hosts you fully trust. The runner does not sandbox transpiled output.
199
+
200
+ ---
201
+
202
+ ## Contributing
203
+
204
+ When changing the runner (selectors, transpiler options, CDN URLs, logging), update this README in the same commit.
205
+
206
+ ### Release
207
+
208
+ ```bash
209
+ npm version patch # or minor / major
210
+ npm publish --access public
211
+ ```
212
+
213
+ The unversioned `https://esm.sh/application-typescript` URL always resolves to the latest published release.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * In-browser TypeScript runner: finds every
3
+ * <script type="application/typescript" src="./path.ts"></script>
4
+ * (`src` is required — inline body without `src` is ignored). Fetches each URL,
5
+ * transpiles with the `typescript` package from esm.sh, then `import()` a blob URL
6
+ * so the result is a real ES module (top-level `import` works; no eval).
7
+ *
8
+ * Relative imports like `import { x } from "./foo"` work: each file is transpiled
9
+ * separately and specifiers are rewritten to blob URLs (import base is otherwise
10
+ * the blob, so `./foo` would not resolve). Circular relative imports can deadlock.
11
+ *
12
+ * Bare specifiers (`import from "lodash"`) are rewritten to absolute URLs: first
13
+ * match against merged top-level `imports` from every `<script type="importmap">`,
14
+ * otherwise `https://esm.sh/<specifier>`. (Blob modules often ignore import maps;
15
+ * rewriting makes bare imports reliable.) Scoped packages and subpaths work on
16
+ * esm.sh, e.g. `lodash/debounce`.
17
+ */
18
+ import ts from "https://esm.sh/typescript@6.0.3";
19
+
20
+ (async function () {
21
+ const SELECTOR = 'script[type="application/typescript"][src]';
22
+
23
+ /** @type {Record<string, string> | null} */
24
+ let cachedImportsMap = null;
25
+
26
+ /** @type {Map<string, string>} */
27
+ const blobUrlByTsUrl = new Map();
28
+ /** @type {Map<string, Promise<string>>} */
29
+ const inflightBlobUrl = new Map();
30
+
31
+ function showError(msg, cause) {
32
+ if (cause !== undefined) {
33
+ console.error("[application-typescript]", msg, cause);
34
+ } else {
35
+ console.error("[application-typescript]", msg);
36
+ }
37
+ }
38
+
39
+ function getTs() {
40
+ return ts?.default ?? ts;
41
+ }
42
+
43
+ async function transpileModuleSource(fileName, sourceText) {
44
+ const t = getTs();
45
+ const { outputText } = t.transpileModule(sourceText, {
46
+ compilerOptions: {
47
+ target: t.ScriptTarget.ES2022,
48
+ module: t.ModuleKind.ESNext,
49
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
50
+ skipLibCheck: true,
51
+ isolatedModules: true,
52
+ },
53
+ fileName,
54
+ });
55
+ return outputText;
56
+ }
57
+
58
+ /**
59
+ * Resolve a relative module specifier to an absolute .ts URL (same-origin fetch).
60
+ * @param {string} containingTsUrl absolute URL of the current module
61
+ * @param {string} specifier e.g. ./ok ./ok.js ../lib/x
62
+ */
63
+ function resolveTsDependencyUrl(containingTsUrl, specifier) {
64
+ const baseDir = new URL(".", containingTsUrl);
65
+ let spec = specifier;
66
+ if (spec.endsWith(".js")) spec = spec.slice(0, -3);
67
+ if (spec.endsWith(".jsx")) spec = spec.slice(0, -4);
68
+ const u = new URL(spec, baseDir);
69
+ if (!/(?:\.[cm]?ts|\.tsx)$/.test(u.pathname)) {
70
+ u.pathname = u.pathname.endsWith("/") ? `${u.pathname}index.ts` : `${u.pathname}.ts`;
71
+ }
72
+ return u.href;
73
+ }
74
+
75
+ /**
76
+ * Collect relative specifiers as they appear in emitted JS (./ ../ only).
77
+ * @param {string} js
78
+ */
79
+ function collectRelativeSpecifiersInJs(js) {
80
+ const specs = new Set();
81
+ const fromRe = /\bfrom\s*["'](\.\.?\/[^"']+)["']/g;
82
+ const dynRe = /\bimport\s*\(\s*["'](\.\.?\/[^"']+)["']\s*\)/g;
83
+ for (const re of [fromRe, dynRe]) {
84
+ for (const m of js.matchAll(re)) {
85
+ specs.add(m[1]);
86
+ }
87
+ }
88
+ return [...specs];
89
+ }
90
+
91
+ /**
92
+ * Merge `imports` from all import map scripts (later maps override keys).
93
+ * Only top-level `imports` is used (not `scopes`).
94
+ */
95
+ function mergeDocumentImportMaps() {
96
+ /** @type {Record<string, string>} */
97
+ const out = {};
98
+ for (const el of document.querySelectorAll('script[type="importmap"]')) {
99
+ try {
100
+ const j = JSON.parse(el.textContent || "{}");
101
+ if (j.imports && typeof j.imports === "object") {
102
+ Object.assign(out, j.imports);
103
+ }
104
+ } catch (e) {
105
+ console.warn("[application-typescript] skipped invalid importmap", e);
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function getImportsMap() {
112
+ if (!cachedImportsMap) cachedImportsMap = mergeDocumentImportMaps();
113
+ return cachedImportsMap;
114
+ }
115
+
116
+ /**
117
+ * @param {string} spec
118
+ */
119
+ function isBareModuleSpecifier(spec) {
120
+ if (!spec || typeof spec !== "string") return false;
121
+ if (spec.startsWith(".") || spec.startsWith("/")) return false;
122
+ if (/^(https?:|data:|blob:)/i.test(spec)) return false;
123
+ return true;
124
+ }
125
+
126
+ /**
127
+ * Collect bare specifiers in emitted JS (not ./ ../ not URLs).
128
+ * @param {string} js
129
+ */
130
+ function collectBareSpecifiersInJs(js) {
131
+ const specs = new Set();
132
+ const fromRe = /\bfrom\s*["']([^"']+)["']/g;
133
+ const dynRe = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
134
+ for (const m of js.matchAll(fromRe)) {
135
+ if (isBareModuleSpecifier(m[1])) specs.add(m[1]);
136
+ }
137
+ for (const m of js.matchAll(dynRe)) {
138
+ if (isBareModuleSpecifier(m[1])) specs.add(m[1]);
139
+ }
140
+ return [...specs];
141
+ }
142
+
143
+ /**
144
+ * @param {string} spec bare specifier, e.g. lodash or @scope/pkg
145
+ * @param {Record<string, string>} importsMap
146
+ */
147
+ function resolveBareModuleSpecifier(spec, importsMap) {
148
+ const mapped = importsMap[spec];
149
+ if (mapped) {
150
+ try {
151
+ return new URL(mapped, location.href).href;
152
+ } catch {
153
+ return mapped;
154
+ }
155
+ }
156
+ return `https://esm.sh/${spec}`;
157
+ }
158
+
159
+ /**
160
+ * @param {string} js
161
+ * @param {string} fromSpec specifier as in source/emitted string
162
+ * @param {string} toBlobUrl
163
+ */
164
+ function rewriteSpecifier(js, fromSpec, toBlobUrl) {
165
+ const esc = fromSpec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ const to = JSON.stringify(toBlobUrl);
167
+ let out = js.replace(
168
+ new RegExp(`(\\bfrom\\s*)["']${esc}["']`, "g"),
169
+ `$1${to}`,
170
+ );
171
+ out = out.replace(
172
+ new RegExp(`(\\bimport\\s*\\(\\s*)["']${esc}["']`, "g"),
173
+ `$1${to}`,
174
+ );
175
+ return out;
176
+ }
177
+
178
+ /**
179
+ * Transpile a TypeScript module and its relative dependency tree; return a blob URL.
180
+ * @param {string} resolvedTsUrl
181
+ */
182
+ async function getBlobUrlForTsModule(resolvedTsUrl) {
183
+ const cached = blobUrlByTsUrl.get(resolvedTsUrl);
184
+ if (cached) return cached;
185
+
186
+ const pending = inflightBlobUrl.get(resolvedTsUrl);
187
+ if (pending) return pending;
188
+
189
+ const p = (async () => {
190
+ const res = await fetch(resolvedTsUrl, { cache: "no-cache" });
191
+ if (!res.ok) {
192
+ throw new Error(`${resolvedTsUrl}: HTTP ${res.status}`);
193
+ }
194
+ const source = await res.text();
195
+ let js = await transpileModuleSource(resolvedTsUrl, source);
196
+
197
+ const relSpecs = collectRelativeSpecifiersInJs(js).sort(
198
+ (a, b) => b.length - a.length,
199
+ );
200
+
201
+ for (const spec of relSpecs) {
202
+ const depTsUrl = resolveTsDependencyUrl(resolvedTsUrl, spec);
203
+ const depBlob = await getBlobUrlForTsModule(depTsUrl);
204
+ js = rewriteSpecifier(js, spec, depBlob);
205
+ }
206
+
207
+ const importsMap = getImportsMap();
208
+ const bareSpecs = collectBareSpecifiersInJs(js).sort(
209
+ (a, b) => b.length - a.length,
210
+ );
211
+ for (const spec of bareSpecs) {
212
+ const url = resolveBareModuleSpecifier(spec, importsMap);
213
+ js = rewriteSpecifier(js, spec, url);
214
+ }
215
+
216
+ const label = resolvedTsUrl.replace(/\n/g, " ");
217
+ const blob = new Blob([`${js}\n//# sourceURL=${label}\n`], {
218
+ type: "text/javascript",
219
+ });
220
+ const blobUrl = URL.createObjectURL(blob);
221
+ blobUrlByTsUrl.set(resolvedTsUrl, blobUrl);
222
+ return blobUrl;
223
+ })();
224
+
225
+ inflightBlobUrl.set(resolvedTsUrl, p);
226
+ try {
227
+ return await p;
228
+ } finally {
229
+ inflightBlobUrl.delete(resolvedTsUrl);
230
+ }
231
+ }
232
+
233
+ async function runOneScript(el) {
234
+ const src = el.getAttribute("src");
235
+ if (!src) return;
236
+ const resolved = new URL(src, location.href).href;
237
+ const entryBlobUrl = await getBlobUrlForTsModule(resolved);
238
+ await import(entryBlobUrl);
239
+ }
240
+
241
+ const scripts = [...document.querySelectorAll(SELECTOR)];
242
+ if (scripts.length === 0) {
243
+ showError(
244
+ `No matching scripts. Add e.g. <script type="application/typescript" src="/src/app.ts"></script> before the <script type="module"> that loads this runner (from esm.sh or your host).`,
245
+ );
246
+ } else {
247
+ for (const el of scripts) {
248
+ try {
249
+ await runOneScript(el);
250
+ } catch (e) {
251
+ showError(
252
+ `TypeScript runner: ${e instanceof Error ? e.message : String(e)}`,
253
+ e,
254
+ );
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ })().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "application-typescript",
3
+ "version": "1.0.0",
4
+ "description": "Browser loader: run TypeScript from script[type=application/typescript][src] without a bundler",
5
+ "type": "module",
6
+ "main": "./application-typescript.js",
7
+ "module": "./application-typescript.js",
8
+ "exports": {
9
+ ".": {
10
+ "browser": "./application-typescript.js",
11
+ "default": "./application-typescript.js"
12
+ }
13
+ },
14
+ "sideEffects": true,
15
+ "engines": {
16
+ "browser": "*"
17
+ },
18
+ "files": [
19
+ "application-typescript.js",
20
+ "README.md"
21
+ ],
22
+ "keywords": [
23
+ "typescript",
24
+ "browser",
25
+ "esm",
26
+ "loader",
27
+ "transpile",
28
+ "no-bundler",
29
+ "prototype",
30
+ "cdn",
31
+ "esm.sh",
32
+ "runtime",
33
+ "browser-only",
34
+ "transpiler",
35
+ "blob-url",
36
+ "import-map"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/slavahatnuke/application-typescript.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/slavahatnuke/application-typescript/issues"
45
+ },
46
+ "homepage": "https://github.com/slavahatnuke/application-typescript#readme",
47
+ "scripts": {
48
+ "serve": "npx -y serve . -l 5123 --no-port-switching",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "npx -y serve ."
51
+ },
52
+ "devDependencies": {
53
+ "@types/lodash": "^4.17.16",
54
+ "htm": "3.1.1",
55
+ "preact": "10.24.3",
56
+ "typescript": "5.7"
57
+ }
58
+ }