🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 9 Min Lesezeit
0

Migrating to TypeScript Without a Bundler: The Radar College Story

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

: a browser build of Babel. The script loads the .tsx file via a <script type="text/babel" data-presets="react,typescript"> tag, Babel compiles it to JavaScript on the fly, and React mounts it in the DOM.



The cost: ~400 KB on first load. But the Service Worker caches everything (cache-first for local assets, network-first for CDNs), so it's transparent from the second visit onwards. In file:// mode, the SW gracefully disables itself — Babel loads from CDN, the rest is in the HTML.



The real problem is that Babel Standalone has TypeScript-specific bugs that don't exist in regular Babel. Two in particular cost me time:




CODE
// ❌ Babel Standalone can't handle useState<T>(value)
const [screen, setScreen] = useState<QuizPhase>('home');
// → Parse error: the < is interpreted as JSX

// ❌ Inline casts also break
const elapsed = (new Date() as any) - startTime;









CODE
// ✅ Workaround: factory functions to type useState
const initialPhase = (): QuizPhase => 'home';
const [screen, setScreen] = useState(initialPhase);

// ✅ Workaround: intermediate variables for casts
const now: number = Date.now();
const elapsed = now - startTime;






This pattern was applied to all 10 useState calls in the main component. Not elegant, but it works and TypeScript infers the types correctly. The trade-off is acceptable: lose the idiomatic syntax, keep the type safety.






200 lines of types without import/export



Babel Standalone doesn't resolve imports. No import { Question } from './types'. All types must be globally scoped, declared as ambient in a types.ts file loaded before the application.




CODE
// types.ts — global ambient declarations (excerpts)
type Subject = 'maths' | 'physique' | 'svt';
type Level = '6eme' | '5eme' | '4eme' | '3eme';
type QuizKey = `${Subject}-${Level}`; // template literal type
type QuizPhase = 'home' | 'quiz' | 'report';

interface Question {
id: string;
text: string;
options: string[];
correct: number;
domain: string;
hint?: string;
method?: string;
}

interface DomainAnalysis {
domain: string;
label: string;
total: number;
correct: number;
pct: number;
status: 'acquired' | 'fragile' | 'not-acquired';
}

interface Attempt {
date: string;
score: number;
weighted: number;
total: number;
domains: DomainAnalysis[];
mode: 'training' | 'exam';
}






The file is ~200 lines and covers everything: data types (Question, QuizConfig, Attempt), runtime state (AnswersMap, TimingsMap, HintsMap — all Record<string, ...>), results (DomainAnalysis, AnalyzeResult), and React props for each component.



The most useful type is QuizKey: a template literal type `${Subject}-${Level}` encoding the 12 valid combinations. Impossible to pass 'maths-cm2' or 'french-3eme' without TypeScript complaining. This type alone would have prevented the duplicated SUBJECT.id bug.






A 50-line build system



The build system is a 50-line bash script with some inline Python. No webpack, no vite, no esbuild. Just marker replacement in an HTML template.




CODE
#!/bin/bash
# build.sh — inline everything into a single index.html

TEMPLATE="index.html"
OUTPUT="dist/index.html"

# 1. Read the template
cp "$TEMPLATE" "$OUTPUT"

# 2. Inline CSS
CSS=$(cat app.css)
# Python for replacement (sed struggles with multiline)
python3 -c "
import sys
content = open('
$OUTPUT').read()
css = open('app.css').read()
content = content.replace('/* __APP_CSS__ */', css)
# Inline each quiz file
import glob
for f in sorted(glob.glob('quizzes/*.tsx')):
key = f.replace('quizzes/','').replace('.tsx','')
marker = f'/* __QUIZ_{key.upper()}__ */'
content = content.replace(marker, open(f).read())
# Inline app.tsx
app = open('app.tsx').read()
content = content.replace('/* __APP_TSX__ */', app)
open('
$OUTPUT', 'w').write(content)
"







The result: a ~300 KB index.html that contains everything — CSS, TypeScript for all 12 quizzes, application code. One file to deploy. Babel Standalone compiles it in the browser at load time. The Service Worker caches it. Done.



Is it optimal? No. Is it sufficient for an app used by a handful of students? Absolutely. And more importantly, it's understandable. Anyone can read build.sh and understand what it does. Try that with a 200-line webpack.config.js.






The IIFE and the mount race condition



Radar College's architecture is a bit unusual: routing is vanilla JavaScript in the index.html, and the React app lives in a separate app.tsx, wrapped in an IIFE for scope isolation.



The problem is that Babel Standalone compiles TSX asynchronously, but the vanilla router is synchronous. When the user lands on #/3eme/maths/quiz, the router calls window.mountQuizApp('maths-3eme') — except the function doesn't exist yet because Babel hasn't finished compiling.




CODE
// Solution: pending queue on window
// Router side (vanilla JS):
if (typeof window.mountQuizApp === 'function') {
window.mountQuizApp(key);
} else {
window.__pendingQuizMount = key; // "I wanted to mount this"
}

// React side (app.tsx, after Babel compilation):
window.mountQuizApp = (key: string) => {
setActiveQuiz(key as QuizKey);
root.render(<App />);
};

// Check for pending mount
if (window.__pendingQuizMount) {
window.mountQuizApp(window.__pendingQuizMount);
delete window.__pendingQuizMount;
}






Pure plumbing for cross-world communication. In a bundled project, everything would share the same scope and this problem wouldn't exist. But the zero-build constraint forces you to make explicit things you normally take for granted.






48 E2E tests with Playwright



No unit tests. Only end-to-end tests. The choice may surprise, but it makes sense for this type of project: the value lies in user interactions (selecting an answer, viewing a score, retrieving history), not in isolated pure functions.



The 48 scenarios cover 8 areas:



Category



Tests



What's verified



Wizard / Landing



11



Name input, level selection, memory across visits, student switching



Quiz



11



Training/exam modes, hints, timer, keyboard navigation



Report



3



Score, per-chapter diagnosis, radar, revision plan



Dashboard



4



Access, empty history, badges



PWA



3



Manifest, Service Worker, icons



SPA Routing



8



Hash URLs, deep links, back navigation, invalid hash fallback



Design / a11y



4



Tap targets ≥ 44px, dark mode, dyslexia font, zero console errors



User journeys



4



Full flow, memory persistence, quiz-to-quiz transitions



The most useful test during the TypeScript migration was "zero console errors". Playwright intercepts all JavaScript errors, and the test fails if it finds a single one. Every typing error that slipped past Babel got caught there. It's a brutal but effective safety net when migrating JS to TS: if it compiles and the "zero console error" test passes, the migration hasn't broken anything.




CODE
// Test: no console errors during full user journey
test('no console errors during full user journey', async ({ page }) => {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});

// Full flow: wizard → quiz → report → dashboard
await page.goto('/');
await page.fill('#student-name', 'Test');
await page.click('[data-level="3eme"]');
await page.click('[data-subject="maths"]');
// ... complete the quiz ...

expect(errors).toEqual([]);
});






Tests run in GitHub Actions on every push. The CI also runs npx tsc --noEmit — type checking without code emission, since Babel handles compilation. Two validation layers: types statically, behavior via E2E.






What the migration revealed



The TypeScript migration was done in a single PR: 22 files, +709/-182 lines. Three review iterations, progressive score from 7.5 to 8.2/10. Here's what the types revealed in code that "worked":



Three quiz files had the same SUBJECT.id. maths-4eme, physique-4eme, and svt-4eme all declared id: 'maths'. In JavaScript, no error — quizzes loaded, questions displayed, but the dashboard mixed up results for all three 4th-grade subjects. The kind of bug a user would report as "my scores are weird" without being able to explain why.



Arithmetic on Date objects. new Date(a) - new Date(b) works in JavaScript because Date objects are implicitly converted to numbers via valueOf(). TypeScript rightfully refuses this implicit conversion: if a is undefined (unanswered question), the result is NaN, and the average time-per-question calculation becomes silently wrong.



Ghost props. Two components received props that had been renamed in the parent three weeks earlier. The code worked because the props were effectively optional (JavaScript doesn't complain when you pass extra keys to an object). But it meant the component used its default value instead of the actual one. TypeScript caught the mismatch.






Conclusion



The lesson isn't "use TypeScript" — everyone knows that already. The lesson is that a project's architectural constraints (zero-build, file://, no bundler) aren't an excuse to skip type safety. Babel Standalone + ambient declarations isn't the ideal solution, but it's a solution that works — and the bugs found during migration proved it was worth the effort.



The source code is on GitHub. All 48 tests pass. Types compile. And my nephew still doesn't know that the platform he uses every evening compiles TypeScript in his browser.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Migrating to TypeScript Without a Bundler: The Radar College Story

Thematisch verwandte Begriffe: Migrating, TypeScript, Without, Bundler · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...