Skip to content

Commit 6cf60e2

Browse files
authored
Merge pull request #42724 from twbs/issue-42620
Migrate the JavaScript codebase to TypeScript
2 parents 565eee6 + 0ac9597 commit 6cf60e2

195 files changed

Lines changed: 3584 additions & 1279 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.babelrc.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ export default {
77
bugfixes: true,
88
modules: false
99
}
10+
],
11+
[
12+
'@babel/preset-typescript',
13+
{
14+
// `declare` fields carry types for constructor-assigned properties
15+
// without emitting runtime field definitions
16+
allowDeclareFields: true
17+
}
1018
]
1119
]
1220
}

.bundlewatch.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@
3434
},
3535
{
3636
"path": "./dist/js/bootstrap.bundle.js",
37-
"maxSize": "88.0 kB"
37+
"maxSize": "88.25 kB"
3838
},
3939
{
4040
"path": "./dist/js/bootstrap.bundle.min.js",
4141
"maxSize": "54.75 kB"
4242
},
4343
{
4444
"path": "./dist/js/bootstrap.js",
45-
"maxSize": "59.25 kB"
45+
"maxSize": "59.5 kB"
4646
},
4747
{
4848
"path": "./dist/js/bootstrap.min.js",

AGENTS.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Guidance for AI coding agents working in this repository. Tool-agnostic; `CLAUDE.md` points here.
44

55
Bootstrap v6.0.0-alpha1 — CSS/JS framework.
6-
Sass source in `scss/`, JS source in `js/src/`, docs site in `site/` (Astro 5 + MDX).
6+
Sass source in `scss/`, TypeScript source in `js/src/`, docs site in `site/` (Astro 5 + MDX).
77

88
## Quick commands
99

@@ -28,14 +28,17 @@ Sass source in `scss/`, JS source in `js/src/`, docs site in `site/` (Astro 5 +
2828
- Modern CSS: `color-mix()`, `light-dark()`, range media queries (`@media (width >= 1024px)`)
2929
- Doc markers: `// scss-docs-start name` / `// scss-docs-end name` for docs extraction
3030

31-
## JavaScript conventions
31+
## JavaScript/TypeScript conventions
3232

33-
- ESM-only, no semicolons, 2-space indent
34-
- Components extend `BaseComponent` (which extends `Config`)
33+
- Source is TypeScript (`js/src/**/*.ts`, entry `js/index.ts`); ESM-only, no semicolons, 2-space indent
34+
- Imports use `.js` extensions (standard TS ESM style); the build resolves them to `.ts` via `build/rollup-plugin-ts-resolve.cjs`
35+
- Strict tsconfig with `verbatimModuleSyntax` + `erasableSyntaxOnly`; Babel (`@babel/preset-typescript`) strips types, `tsc` only type-checks and emits `.d.ts` (`npm run js-typecheck`, `npm run js-compile-types`)
36+
- Components extend `BaseComponent` (which extends `Config`); per-component config `type XxxConfig` typed off `Default`, refined on the class via `declare _config: XxxConfig`
37+
- Instance fields use `declare` (no runtime emit); constructor `element` param stays optional to keep `typeof Component` assignable to `typeof BaseComponent`
3538
- Constants: `NAME`, `DATA_KEY`, `EVENT_KEY`, `VERSION`
36-
- DOM utilities via `dom/event-handler.js`, `dom/selector-engine.js`, `dom/manipulator.js`
39+
- DOM utilities via `dom/event-handler.ts`, `dom/selector-engine.ts`, `dom/manipulator.ts`
3740
- Floating UI for positioning (dropdown, tooltip, popover)
38-
- Tests: Jasmine + Karma, specs in `js/tests/unit/*.spec.js`
41+
- Tests: Jasmine + Karma, specs in `js/tests/unit/*.spec.js` (plain JS, bundled against the TS sources); type-level API tests in `js/tests/types/`
3942

4043
## Docs conventions
4144

build/build-plugins.mjs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,43 @@
66
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
77
*/
88

9+
import fs from 'node:fs/promises'
910
import path from 'node:path'
1011
import { fileURLToPath } from 'node:url'
1112
import { babel } from '@rollup/plugin-babel'
1213
import { globby } from 'globby'
1314
import { rollup } from 'rollup'
1415
import banner from './banner.mjs'
16+
import tsResolve from './rollup-plugin-ts-resolve.cjs'
1517

1618
const __filename = fileURLToPath(import.meta.url)
1719
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1820

1921
const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/')
20-
const jsFiles = await globby(`${sourcePath}/**/*.js`)
22+
const distPath = path.resolve(__dirname, '../js/dist/').replace(/\\/g, '/')
23+
const tsFiles = await globby(`${sourcePath}/**/*.ts`)
2124

2225
// Array which holds the resolved plugins
2326
const resolvedPlugins = []
2427

25-
for (const file of jsFiles) {
28+
for (const file of tsFiles) {
2629
resolvedPlugins.push({
2730
src: file,
28-
dist: file.replace('src', 'dist'),
29-
fileName: path.basename(file)
31+
dist: file.replace(sourcePath, distPath).replace(/\.ts$/, '.js'),
32+
fileName: path.basename(file).replace(/\.ts$/, '.js')
3033
})
3134
}
3235

33-
const build = async plugin => {
36+
const build = async (plugin, outputOptions = {}) => {
3437
const bundle = await rollup({
3538
input: plugin.src,
3639
plugins: [
40+
tsResolve(),
3741
babel({
3842
// Only transpile our source code
3943
exclude: 'node_modules/**',
44+
// Transpile the TypeScript sources too
45+
extensions: ['.js', '.mjs', '.ts'],
4046
// Include the helpers in each file, at most one copy of each
4147
babelHelpers: 'bundled'
4248
})
@@ -49,12 +55,33 @@ const build = async plugin => {
4955
format: 'esm',
5056
sourcemap: true,
5157
generatedCode: 'es2015',
52-
file: plugin.dist
58+
file: plugin.dist,
59+
...outputOptions
5360
})
5461

5562
console.log(`Built ${plugin.fileName}`)
5663
}
5764

65+
// Build the package entry point next to the plugins. Its re-exports point at
66+
// `./src/*.ts` sources, so rewrite them to the sibling compiled plugins, and
67+
// derive the entry's type declarations the same way (the plugins' own
68+
// declarations are emitted by `js-compile-types`).
69+
const buildIndex = async () => {
70+
const src = path.resolve(__dirname, '../js/index.ts')
71+
72+
await build(
73+
{ src, dist: path.resolve(__dirname, '../js/dist/index.js'), fileName: 'index.js' },
74+
// The re-exported `./src/*.js` modules live next to the built index —
75+
// point at them by filename regardless of how Rollup normalized the ids
76+
{ paths: id => `./${path.basename(id)}` }
77+
)
78+
79+
const source = await fs.readFile(src, 'utf8')
80+
const declarations = source.replace(/'\.\/src\//g, '\'./').replace('Bootstrap index.ts', 'Bootstrap index.d.ts')
81+
await fs.writeFile(path.resolve(__dirname, '../js/dist/index.d.ts'), declarations)
82+
console.log('Built index.d.ts')
83+
}
84+
5885
(async () => {
5986
try {
6087
const basename = path.basename(__filename)
@@ -63,7 +90,7 @@ const build = async plugin => {
6390
console.log('Building individual plugins...')
6491
console.time(timeLabel)
6592

66-
await Promise.all(Object.values(resolvedPlugins).map(plugin => build(plugin)))
93+
await Promise.all([...resolvedPlugins.map(plugin => build(plugin)), buildIndex()])
6794

6895
console.timeEnd(timeLabel)
6996
} catch (error) {

build/change-version.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-r
1717
const FILES = [
1818
'README.md',
1919
'config.yml',
20-
'js/src/base-component.js',
20+
'js/src/base-component.ts',
2121
'package.js',
2222
'scss/_banner.scss',
2323
'site/data/docs-versions.yml'

build/rollup-plugin-ts-resolve.cjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict'
2+
3+
/*!
4+
* Rollup plugin that resolves relative `.js` import specifiers to their
5+
* TypeScript sources. Our TS files import siblings with the standard
6+
* ESM-style `.js` extension (the same way tsc resolves them), so when Rollup
7+
* bundles straight from `js/src`, a specifier like `./util/index.js` must be
8+
* mapped to the `./util/index.ts` file on disk.
9+
* Copyright 2011-2026 The Bootstrap Authors
10+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
11+
*/
12+
13+
const fs = require('node:fs')
14+
const path = require('node:path')
15+
16+
const tsResolve = () => {
17+
return {
18+
name: 'ts-resolve',
19+
resolveId(source, importer) {
20+
if (!importer || !source.startsWith('.') || !source.endsWith('.js')) {
21+
return null
22+
}
23+
24+
const tsPath = path.resolve(path.dirname(importer), `${source.slice(0, -3)}.ts`)
25+
return fs.existsSync(tsPath) ? tsPath : null
26+
}
27+
}
28+
}
29+
30+
module.exports = tsResolve

build/rollup.config.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { babel } from '@rollup/plugin-babel'
55
import { nodeResolve } from '@rollup/plugin-node-resolve'
66
import replace from '@rollup/plugin-replace'
77
import banner from './banner.mjs'
8+
import tsResolve from './rollup-plugin-ts-resolve.cjs'
89

910
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1011

@@ -13,9 +14,12 @@ const BUNDLE = process.env.BUNDLE === 'true'
1314
let destinationFile = 'bootstrap'
1415
const external = ['@floating-ui/dom', 'vanilla-calendar-pro']
1516
const plugins = [
17+
tsResolve(),
1618
babel({
1719
// Only transpile our source code
1820
exclude: 'node_modules/**',
21+
// Transpile the TypeScript sources too
22+
extensions: ['.js', '.mjs', '.ts'],
1923
// Include the helpers in the bundle, at most one copy of each
2024
babelHelpers: 'bundled'
2125
})
@@ -35,7 +39,7 @@ if (BUNDLE) {
3539
}
3640

3741
const rollupConfig = {
38-
input: path.resolve(__dirname, '../js/index.js'),
42+
input: path.resolve(__dirname, '../js/index.ts'),
3943
output: {
4044
banner: banner(),
4145
file: path.resolve(__dirname, `../dist/js/${destinationFile}.js`),

build/tsconfig.dts.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": false,
5+
"emitDeclarationOnly": true,
6+
"declaration": true,
7+
"declarationMap": true,
8+
"rootDir": "../js/src",
9+
"outDir": "../js/dist"
10+
},
11+
"include": ["../js/src/**/*.ts"]
12+
}

0 commit comments

Comments
 (0)