@openload28/create-tt 1.0.0-dev.20260828.263

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,34 @@
1
+ # @openload28/create-tt
2
+
3
+ Create a ready-to-run Vite + TypeScript + tt project:
4
+
5
+ ```sh
6
+ bunx @openload28/create-tt@next my-app
7
+ ```
8
+
9
+ Add tt to an existing TypeScript project. The initializer detects Vite,
10
+ Rollup, Rolldown, webpack, Rspack, esbuild, or Farm from `package.json`:
11
+
12
+ ```sh
13
+ bunx @openload28/create-tt@next init
14
+ ```
15
+
16
+ The initializer updates `package.json` structurally. For bundlers with a
17
+ declarative config, it generates an `tt.*.config.mjs` wrapper instead of
18
+ rewriting arbitrary user code. Existing scripts and config files stay intact.
19
+ Use `--no-install` in CI or when dependencies will be installed later.
20
+ New projects use Bun for dependency installation and scripts. Existing projects
21
+ keep the package manager declared in `package.json` or selected by their lockfile.
22
+
23
+ For packages published to a private or local npm-compatible registry, pass the
24
+ registry through the entire bootstrap:
25
+
26
+ ```sh
27
+ BUN_CONFIG_REGISTRY=http://127.0.0.1:4873 \
28
+ bunx @openload28/create-tt@next my-app --registry http://127.0.0.1:4873
29
+ ```
30
+
31
+ The Nightly initializer installs the `next` channels of
32
+ `@openload28/tt-lang` and `@openload28/unplugin-tt`. The current TypeScript toolchain
33
+ prerequisite is documented in the repository
34
+ [installation guide](https://github.com/load28/tt/blob/main/docs/getting-started.md).
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/installer.js'
4
+
5
+ run(process.argv.slice(2)).catch((error) => {
6
+ console.error(`@openload28/create-tt: ${error.message}`)
7
+ process.exitCode = 1
8
+ })
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@openload28/create-tt",
3
+ "version": "1.0.0-dev.20260828.263",
4
+ "description": "Create a new tt project or add tt to an existing TypeScript project",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "create-tt": "bin/create-tt.js"
13
+ },
14
+ "files": [
15
+ "bin/create-tt.js",
16
+ "src/installer.js",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "test": "node --test"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/load28/tt",
28
+ "directory": "packages/create-tt"
29
+ },
30
+ "keywords": [
31
+ "tt",
32
+ "typescript",
33
+ "scaffold",
34
+ "vite"
35
+ ]
36
+ }
@@ -0,0 +1,337 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
3
+ import { basename, join, relative, resolve } from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
5
+
6
+ const ownManifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
7
+ const ttChannel = dependencyChannel(ownManifest.version)
8
+ const versions = {
9
+ '@openload28/tt-lang': ttChannel,
10
+ '@openload28/unplugin-tt': ttChannel,
11
+ vite: '^8.0.0',
12
+ // ttc drives the TypeScript the project installs and looks nowhere else,
13
+ // so a scaffolded project has to install one — the exact version this
14
+ // repository tests against, which is the whole point: a user runs the
15
+ // compiler against the TypeScript it was verified with, and an upstream
16
+ // nightly cannot change their build overnight. Not `7`: declaration
17
+ // output (`ttc --types`, the editor's `.tt.d.ts` sidecars) needs the emit
18
+ // API that arrived in 7.1, and `7` resolves to the 7.0 line because
19
+ // ranges do not match prereleases (TASK-256). When 7.1 is released this
20
+ // moves to `7` everywhere at once, held by
21
+ // npm/scripts/typescript-version.test.mjs.
22
+ typescript: "7.1.0-dev.20260826.1",
23
+ }
24
+
25
+ export function dependencyChannel(packageVersion) {
26
+ if (/-dev\./.test(packageVersion)) return 'next'
27
+ if (/-rc(?:\.|$)/.test(packageVersion)) return 'rc'
28
+ return 'latest'
29
+ }
30
+
31
+ const bundlers = {
32
+ vite: {
33
+ packages: ['vite'],
34
+ configs: ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs'],
35
+ module: '@openload28/unplugin-tt/vite',
36
+ wrapper: 'tt.vite.config.mjs',
37
+ commands: { dev: 'vite', build: 'vite build' },
38
+ },
39
+ rollup: {
40
+ packages: ['rollup'],
41
+ configs: ['rollup.config.ts', 'rollup.config.js', 'rollup.config.mjs'],
42
+ module: '@openload28/unplugin-tt/rollup',
43
+ wrapper: 'tt.rollup.config.mjs',
44
+ commands: { dev: 'rollup --watch', build: 'rollup' },
45
+ },
46
+ rolldown: {
47
+ packages: ['rolldown'],
48
+ configs: ['rolldown.config.ts', 'rolldown.config.js', 'rolldown.config.mjs'],
49
+ module: '@openload28/unplugin-tt/rolldown',
50
+ wrapper: 'tt.rolldown.config.mjs',
51
+ commands: { dev: 'rolldown --watch', build: 'rolldown' },
52
+ },
53
+ webpack: {
54
+ packages: ['webpack'],
55
+ configs: ['webpack.config.ts', 'webpack.config.js', 'webpack.config.cjs', 'webpack.config.mjs'],
56
+ module: '@openload28/unplugin-tt/webpack',
57
+ wrapper: 'tt.webpack.config.mjs',
58
+ commands: { dev: 'webpack serve', build: 'webpack' },
59
+ },
60
+ rspack: {
61
+ packages: ['@rspack/core'],
62
+ configs: ['rspack.config.ts', 'rspack.config.js', 'rspack.config.mjs'],
63
+ module: '@openload28/unplugin-tt/rspack',
64
+ wrapper: 'tt.rspack.config.mjs',
65
+ commands: { dev: 'rspack serve', build: 'rspack build' },
66
+ },
67
+ farm: {
68
+ packages: ['@farmfe/core'],
69
+ configs: ['farm.config.ts', 'farm.config.js', 'farm.config.mjs'],
70
+ module: '@openload28/unplugin-tt/farm',
71
+ wrapper: 'tt.farm.config.mjs',
72
+ commands: { dev: 'farm start', build: 'farm build' },
73
+ },
74
+ esbuild: {
75
+ packages: ['esbuild'],
76
+ configs: [],
77
+ module: '@openload28/unplugin-tt/esbuild',
78
+ },
79
+ }
80
+
81
+ export async function run(argv, io = console) {
82
+ const options = parseArguments(argv)
83
+ if (options.help) {
84
+ io.log(help)
85
+ return
86
+ }
87
+ const result = options.command === 'init'
88
+ ? await initializeExisting(options)
89
+ : await createProject(options)
90
+
91
+ if (options.install) installDependencies(result.root, result.packageManager, options.registry)
92
+ printResult(result, io)
93
+ }
94
+
95
+ export async function createProject(options) {
96
+ const root = resolve(options.directory ?? 'my-tt-app')
97
+ if (existsSync(root) && (await readdir(root)).length > 0) {
98
+ throw new Error(`target directory is not empty: ${root}`)
99
+ }
100
+ await mkdir(join(root, 'src'), { recursive: true })
101
+ const packageManager = options.packageManager ?? 'bun'
102
+ const name = packageName(basename(root))
103
+ const manifest = {
104
+ name,
105
+ private: true,
106
+ version: '0.0.0',
107
+ type: 'module',
108
+ scripts: {
109
+ dev: 'vite',
110
+ build: 'ttc --check-types src && vite build',
111
+ check: 'ttc --check-types src',
112
+ },
113
+ devDependencies: {
114
+ '@openload28/tt-lang': versions['@openload28/tt-lang'],
115
+ '@openload28/unplugin-tt': versions['@openload28/unplugin-tt'],
116
+ typescript: versions.typescript,
117
+ vite: versions.vite,
118
+ },
119
+ }
120
+ await writeJson(join(root, 'package.json'), manifest)
121
+ await writeJson(join(root, 'tsconfig.json'), tsconfig())
122
+ await writeFile(join(root, 'vite.config.ts'), viteConfig)
123
+ await writeFile(join(root, 'index.html'), indexHtml)
124
+ await writeFile(join(root, 'src/main.ts'), "import './app.tt'\n")
125
+ await writeFile(join(root, 'src/app.tt'), starterSource)
126
+ await writeFile(join(root, '.gitignore'), 'node_modules/\ndist/\n.tt-types/\n')
127
+ if (options.registry) {
128
+ await writeFile(join(root, 'bunfig.toml'), `[install]\nregistry = ${JSON.stringify(options.registry)}\n`)
129
+ }
130
+ return { root, packageManager, mode: 'create', bundler: 'vite', files: ['src/main.ts', 'src/app.tt', 'vite.config.ts'] }
131
+ }
132
+
133
+ export async function initializeExisting(options) {
134
+ const root = resolve(options.directory ?? '.')
135
+ const manifestPath = join(root, 'package.json')
136
+ if (!existsSync(manifestPath)) throw new Error(`package.json not found in ${root}`)
137
+
138
+ const source = await readFile(manifestPath, 'utf8')
139
+ const manifest = JSON.parse(source)
140
+ const packageManager = options.packageManager ?? detectPackageManager(root, manifest)
141
+ const bundler = options.bundler === 'auto' ? detectBundler(manifest) : options.bundler
142
+ const devDependencies = manifest.devDependencies ?? {}
143
+ devDependencies['@openload28/tt-lang'] ??= versions['@openload28/tt-lang']
144
+ devDependencies.typescript ??= versions.typescript
145
+ manifest.devDependencies = devDependencies
146
+ manifest.scripts ??= {}
147
+ manifest.scripts['tt:check'] ??= 'ttc --check-types src'
148
+
149
+ const files = []
150
+ let manualModule
151
+ if (bundler && bundler !== 'none') {
152
+ devDependencies['@openload28/unplugin-tt'] ??= versions['@openload28/unplugin-tt']
153
+ const adapter = bundlers[bundler]
154
+ if (!adapter) throw new Error(`unsupported bundler: ${bundler}`)
155
+ const base = adapter.configs.find((file) => existsSync(join(root, file)))
156
+ if (bundler === 'esbuild') {
157
+ manualModule = adapter.module
158
+ } else {
159
+ await writeFile(join(root, adapter.wrapper), wrapperConfig(adapter.module, base))
160
+ files.push(adapter.wrapper)
161
+ manifest.scripts['tt:dev'] ??= `${adapter.commands.dev} --config ${adapter.wrapper}`
162
+ manifest.scripts['tt:build'] ??= `ttc --check-types src && ${adapter.commands.build} --config ${adapter.wrapper}`
163
+ }
164
+ } else {
165
+ manifest.scripts['tt:build'] ??= 'ttc -o .tt-build src'
166
+ }
167
+
168
+ await writeJson(manifestPath, manifest, indentation(source))
169
+ return { root, packageManager, mode: 'init', bundler: bundler ?? 'none', files, manualModule }
170
+ }
171
+
172
+ export function detectBundler(manifest) {
173
+ const dependencies = { ...manifest.dependencies, ...manifest.devDependencies }
174
+ return Object.entries(bundlers).find(([, adapter]) =>
175
+ adapter.packages.some((name) => name in dependencies))?.[0] ?? 'none'
176
+ }
177
+
178
+ function parseArguments(argv) {
179
+ const args = [...argv]
180
+ const options = {
181
+ command: args[0] === 'init' ? args.shift() : 'create',
182
+ install: true,
183
+ bundler: 'auto',
184
+ }
185
+ while (args.length) {
186
+ const arg = args.shift()
187
+ if (arg === '--help' || arg === '-h') options.help = true
188
+ else if (arg === '--no-install') options.install = false
189
+ else if (arg === '--bundler') options.bundler = requiredValue(arg, args)
190
+ else if (arg === '--package-manager') options.packageManager = requiredValue(arg, args)
191
+ else if (arg === '--registry') options.registry = registryUrl(requiredValue(arg, args))
192
+ else if (!arg.startsWith('-') && !options.directory) options.directory = arg
193
+ else throw new Error(`unknown argument: ${arg}`)
194
+ }
195
+ if (!['auto', 'none', ...Object.keys(bundlers)].includes(options.bundler)) {
196
+ throw new Error(`unsupported bundler: ${options.bundler}`)
197
+ }
198
+ if (options.packageManager && !['npm', 'pnpm', 'yarn', 'bun'].includes(options.packageManager)) {
199
+ throw new Error(`unsupported package manager: ${options.packageManager}`)
200
+ }
201
+ if (options.command === 'create' && options.packageManager && options.packageManager !== 'bun') {
202
+ throw new Error('new projects use Bun; --package-manager only applies to init')
203
+ }
204
+ if (options.command === 'create' && !['auto', 'vite'].includes(options.bundler)) {
205
+ throw new Error('new projects use Vite; --bundler only selects vite for create')
206
+ }
207
+ return options
208
+ }
209
+
210
+ function requiredValue(flag, args) {
211
+ const value = args.shift()
212
+ if (!value || value.startsWith('-')) throw new Error(`${flag} needs a value`)
213
+ return value
214
+ }
215
+
216
+ function detectPackageManager(root, manifest = {}) {
217
+ const declared = typeof manifest.packageManager === 'string'
218
+ ? manifest.packageManager.split('@')[0]
219
+ : undefined
220
+ if (['npm', 'pnpm', 'yarn', 'bun'].includes(declared)) return declared
221
+ const locks = [['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun']]
222
+ return locks.find(([file]) => existsSync(join(root, file)))?.[1] ?? 'npm'
223
+ }
224
+
225
+ function installDependencies(root, packageManager, registry) {
226
+ const args = ['install']
227
+ if (registry) args.push('--registry', registry)
228
+ const result = spawnSync(packageManager, args, { cwd: root, stdio: 'inherit' })
229
+ if (result.error) throw result.error
230
+ if (result.status !== 0) throw new Error(`${packageManager} install exited with status ${result.status}`)
231
+ }
232
+
233
+ function registryUrl(value) {
234
+ let url
235
+ try {
236
+ url = new URL(value)
237
+ } catch {
238
+ throw new Error(`invalid registry URL: ${value}`)
239
+ }
240
+ if (!['http:', 'https:'].includes(url.protocol)) throw new Error('registry URL must use http or https')
241
+ if (url.username || url.password) throw new Error('pass registry credentials through Bun configuration or environment variables')
242
+ return url.href
243
+ }
244
+
245
+ function printResult(result, io) {
246
+ const location = relative(process.cwd(), result.root) || '.'
247
+ io.log(result.mode === 'create' ? `Created a tt project in ${location}.` : `Added tt to ${location}.`)
248
+ if (result.files.length) io.log(`Generated: ${result.files.join(', ')}`)
249
+ if (result.manualModule) {
250
+ io.log(`Add tt() from ${result.manualModule} to your esbuild plugins array.`)
251
+ }
252
+ io.log(result.mode === 'create' ? `Run: cd ${location} && ${runScript(result.packageManager, 'dev')}` : `Run: ${runScript(result.packageManager, 'tt:check')}`)
253
+ }
254
+
255
+ function runScript(packageManager, script) {
256
+ return packageManager === 'npm' ? `npm run ${script}` : `${packageManager} run ${script}`
257
+ }
258
+
259
+ function wrapperConfig(moduleName, base) {
260
+ const baseImport = base ? `import base from './${base}'\n` : 'const base = {}\n'
261
+ return `import tt from '${moduleName}'\n${baseImport}
262
+ const addRl = (config = {}) => Array.isArray(config)
263
+ ? config.map(addRl)
264
+ : { ...config, plugins: [tt(), ...(config.plugins ?? [])] }
265
+
266
+ export default typeof base === 'function'
267
+ ? async (...args) => addRl(await base(...args))
268
+ : Promise.resolve(base).then(addRl)
269
+ `
270
+ }
271
+
272
+ function tsconfig() {
273
+ return {
274
+ compilerOptions: {
275
+ target: 'ES2022',
276
+ module: 'Preserve',
277
+ moduleResolution: 'Bundler',
278
+ strict: true,
279
+ noEmit: true,
280
+ skipLibCheck: true,
281
+ },
282
+ include: ['src', 'vite.config.ts'],
283
+ }
284
+ }
285
+
286
+ function packageName(value) {
287
+ const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
288
+ return normalized || 'my-tt-app'
289
+ }
290
+
291
+ function indentation(source) {
292
+ return source.match(/\n([ \t]+)\S/)?.[1] ?? ' '
293
+ }
294
+
295
+ async function writeJson(path, value, space = ' ') {
296
+ await writeFile(path, `${JSON.stringify(value, null, space)}\n`)
297
+ }
298
+
299
+ const viteConfig = `import { defineConfig } from 'vite'
300
+ import tt from '@openload28/unplugin-tt/vite'
301
+
302
+ export default defineConfig({
303
+ plugins: [tt()],
304
+ })
305
+ `
306
+
307
+ const indexHtml = `<!doctype html>
308
+ <html lang="en">
309
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>tt app</title></head>
310
+ <body><main id="app"></main><script type="module" src="/src/main.ts"></script></body>
311
+ </html>
312
+ `
313
+
314
+ const starterSource = `variant Greeting {
315
+ Hello(name: string),
316
+ Welcome,
317
+ }
318
+
319
+ const greeting = Greeting.Hello('tt')
320
+ document.querySelector<HTMLDivElement>('#app')!.textContent = match (greeting) {
321
+ Hello(name) => \`Hello, \${name}!\`,
322
+ Welcome => 'Welcome!',
323
+ }
324
+ `
325
+
326
+ const help = `Create a new tt project or add tt to an existing TypeScript project.
327
+
328
+ Usage:
329
+ bunx @openload28/create-tt@next [directory] [--no-install]
330
+ bunx @openload28/create-tt@next init [directory] [--bundler vite]
331
+
332
+ Options:
333
+ --bundler <auto|none|vite|rollup|rolldown|webpack|rspack|esbuild|farm>
334
+ --package-manager <npm|pnpm|yarn|bun>
335
+ --registry <url> install from an npm-compatible private/local registry
336
+ --no-install
337
+ -h, --help`