@openload28/create-tt 1.0.0-dev.20260928.424 → 1.0.0-dev.20260930.435

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 CHANGED
@@ -20,8 +20,14 @@ bunx @openload28/create-tt@next init
20
20
  The initializer updates `package.json` structurally. For bundlers with a
21
21
  declarative config, it generates an `tt.*.config.mjs` wrapper instead of
22
22
  rewriting arbitrary user code. It also creates `tsconfig.tt.json`, which extends
23
- the existing TypeScript config and declares the content mapper. Existing scripts
24
- and config files stay intact. Re-running init accepts unchanged generated configs;
23
+ the existing TypeScript config and declares the content mapper. A config with
24
+ project `references` (such as Vite's solution-style `tsconfig.json`) gets a
25
+ `*.tt.json` counterpart for every referenced config inside the project, the
26
+ generated configs reference each other the way the originals do, and the
27
+ generated scripts check them with `tsc -b`. The initializer sets `typescript` to
28
+ the TypeScript 7.1 build tt is verified with; when the project named another
29
+ version, it replaces it and prints the change. Existing scripts and config files
30
+ stay intact. Re-running init accepts unchanged generated configs;
25
31
  if a generated config has been customized, init stops before writing any project
26
32
  files.
27
33
  Use `--no-install` in CI or when dependencies will be installed later.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openload28/create-tt",
3
- "version": "1.0.0-dev.20260928.424",
3
+ "version": "1.0.0-dev.20260930.435",
4
4
  "description": "Create a new tt project or add tt to an existing TypeScript project",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/installer.js CHANGED
@@ -1,6 +1,6 @@
1
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'
2
+ import { lstat, mkdir, readFile, readdir, readlink, realpath, writeFile } from 'node:fs/promises'
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
4
4
  import { spawnSync } from 'node:child_process'
5
5
 
6
6
  const ownManifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
@@ -142,14 +142,27 @@ export async function initializeExisting(options) {
142
142
  const bundler = options.bundler === 'auto' ? detectBundler(manifest) : options.bundler
143
143
  const devDependencies = manifest.devDependencies ?? {}
144
144
  devDependencies['@openload28/tt-lang'] ??= versions['@openload28/tt-lang']
145
- devDependencies.typescript ??= versions.typescript
145
+ const updated = []
146
+ const typescriptSection = manifest.dependencies?.typescript !== undefined
147
+ ? manifest.dependencies
148
+ : devDependencies
149
+ const declaredTypeScript = typescriptSection.typescript
150
+ if (declaredTypeScript !== versions.typescript) {
151
+ typescriptSection.typescript = versions.typescript
152
+ if (declaredTypeScript !== undefined) {
153
+ updated.push({ name: 'typescript', from: declaredTypeScript, to: versions.typescript })
154
+ }
155
+ }
146
156
  manifest.devDependencies = devDependencies
147
157
  manifest.scripts ??= {}
148
158
  const typeConfig = 'tsconfig.tt.json'
149
- manifest.scripts['tt:check'] ??= `tsc -p ${typeConfig} --runExternalCode`
150
-
151
- const baseConfig = existsSync(join(root, 'tsconfig.json')) ? './tsconfig.json' : undefined
152
- const generated = [[typeConfig, `${JSON.stringify(tsconfig(baseConfig), null, ' ')}\n`]]
159
+ const generated = []
160
+ const realRoot = await realpath(root)
161
+ const references = existsSync(join(root, 'tsconfig.json'))
162
+ ? await typeConfigGraph(realRoot, await projectConfig(realRoot, join(root, 'tsconfig.json')), generated)
163
+ : (generated.push([typeConfig, `${JSON.stringify(tsconfig(), null, ' ')}\n`]), false)
164
+ const typeCheck = `tsc ${references ? '-b' : '-p'} ${typeConfig} --runExternalCode`
165
+ manifest.scripts['tt:check'] ??= typeCheck
153
166
 
154
167
  const files = []
155
168
  let manualModule
@@ -164,7 +177,7 @@ export async function initializeExisting(options) {
164
177
  generated.push([adapter.wrapper, wrapperConfig(adapter.module, base)])
165
178
  files.push(adapter.wrapper)
166
179
  manifest.scripts['tt:dev'] ??= `${adapter.commands.dev} --config ${adapter.wrapper}`
167
- manifest.scripts['tt:build'] ??= `tsc -p ${typeConfig} --runExternalCode && ${adapter.commands.build} --config ${adapter.wrapper}`
180
+ manifest.scripts['tt:build'] ??= `${typeCheck} && ${adapter.commands.build} --config ${adapter.wrapper}`
168
181
  }
169
182
  } else {
170
183
  manifest.scripts['tt:build'] ??= 'ttc -o .tt-build src'
@@ -173,16 +186,121 @@ export async function initializeExisting(options) {
173
186
  // Validate the complete output set before changing any project files.
174
187
  // Identical generated files make repeated init safe; customized files
175
188
  // require an explicit user decision outside the initializer.
176
- for (const [file, content] of generated) {
177
- const path = join(root, file)
178
- if (existsSync(path) && await readFile(path, 'utf8') !== content) {
189
+ const writes = generated.map(([file, content]) => ({ path: join(root, file), content, replace: false }))
190
+ writes.push({ path: manifestPath, content: jsonText(manifest, indentation(source)), replace: true })
191
+ for (const { path, content, replace } of writes) {
192
+ await assertInsideProject(realRoot, path)
193
+ if (!replace && existsSync(path) && await readFile(path, 'utf8') !== content) {
179
194
  throw new Error(`refusing to overwrite existing config: ${path}`)
180
195
  }
181
196
  }
182
- for (const [file, content] of generated) await writeFile(join(root, file), content)
183
- await writeJson(manifestPath, manifest, indentation(source))
184
- files.push(typeConfig)
185
- return { root, packageManager, mode: 'init', bundler: bundler ?? 'none', files, manualModule }
197
+ for (const { path, content } of writes) await writeFile(path, content)
198
+ files.push(...generated.map(([file]) => file).filter((file) => !files.includes(file)))
199
+ return { root, packageManager, mode: 'init', bundler: bundler ?? 'none', files, manualModule, updated }
200
+ }
201
+
202
+ async function typeConfigGraph(root, configPath, generated, visited = new Set()) {
203
+ visited.add(configPath)
204
+ let config
205
+ try {
206
+ config = parseJsonc(await readFile(configPath, 'utf8'))
207
+ } catch (error) {
208
+ throw new Error(`cannot read ${configPath}: ${error.message}`)
209
+ }
210
+ const directory = dirname(configPath)
211
+ const counterpart = configPath.replace(/\.json$/, '') + '.tt.json'
212
+ const slot = generated.push(undefined) - 1
213
+ const content = tsconfig(`./${basename(configPath)}`)
214
+ const hasReferences = Array.isArray(config.references)
215
+ if (hasReferences) {
216
+ const references = []
217
+ for (const reference of config.references) {
218
+ const lexical = typeof reference?.path === 'string' && referencedConfig(directory, reference.path)
219
+ const target = lexical && existsSync(lexical) && await realpath(lexical)
220
+ if (!target || !insideRoot(root, target)) {
221
+ references.push(reference)
222
+ continue
223
+ }
224
+ if (!visited.has(target)) await typeConfigGraph(root, target, generated, visited)
225
+ const referenced = target.replace(/\.json$/, '') + '.tt.json'
226
+ references.push({ ...reference, path: relativePath(directory, referenced) })
227
+ }
228
+ content.references = references
229
+ }
230
+ generated[slot] = [relative(root, counterpart), `${JSON.stringify(content, null, ' ')}\n`]
231
+ return hasReferences
232
+ }
233
+
234
+ async function projectConfig(root, path) {
235
+ const config = await realpath(path)
236
+ if (!insideRoot(root, config)) throw new Error(`refusing to follow ${path}: it resolves outside the project to ${config}`)
237
+ return config
238
+ }
239
+
240
+ async function assertInsideProject(root, path) {
241
+ const target = await writtenPath(path, new Set())
242
+ if (!insideRoot(root, target)) throw new Error(`refusing to write ${path}: it resolves outside the project to ${target}`)
243
+ }
244
+
245
+ async function writtenPath(path, seen) {
246
+ if (seen.has(path)) throw new Error(`refusing to write ${path}: its symbolic links form a cycle`)
247
+ seen.add(path)
248
+ const entry = await lstat(path).catch(() => null)
249
+ if (entry?.isSymbolicLink()) return writtenPath(resolve(dirname(path), await readlink(path)), seen)
250
+ return entry ? realpath(path) : join(await realpath(dirname(path)), basename(path))
251
+ }
252
+
253
+ function referencedConfig(directory, path) {
254
+ const target = resolve(directory, path)
255
+ return target.endsWith('.json') ? target : join(target, 'tsconfig.json')
256
+ }
257
+
258
+ function insideRoot(root, path) {
259
+ const fromRoot = relative(root, path)
260
+ return fromRoot !== '' && fromRoot !== '..' && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot)
261
+ }
262
+
263
+ function relativePath(from, to) {
264
+ const path = relative(from, to).split('\\').join('/')
265
+ return path === '..' || path.startsWith('../') ? path : `./${path}`
266
+ }
267
+
268
+ export function parseJsonc(source) {
269
+ let text = ''
270
+ let pendingComma = false
271
+ let index = 0
272
+ while (index < source.length) {
273
+ const char = source[index]
274
+ if (source.startsWith('//', index)) {
275
+ while (index < source.length && source[index] !== '\n') index += 1
276
+ continue
277
+ }
278
+ if (source.startsWith('/*', index)) {
279
+ const end = source.indexOf('*/', index + 2)
280
+ index = end === -1 ? source.length : end + 2
281
+ continue
282
+ }
283
+ if (/\s/.test(char)) {
284
+ text += char
285
+ index += 1
286
+ continue
287
+ }
288
+ if (pendingComma && char !== '}' && char !== ']') text += ','
289
+ pendingComma = false
290
+ if (char === ',') {
291
+ pendingComma = true
292
+ index += 1
293
+ } else if (char === '"') {
294
+ let end = index + 1
295
+ while (end < source.length && source[end] !== '"') end += source[end] === '\\' ? 2 : 1
296
+ text += source.slice(index, end + 1)
297
+ index = end + 1
298
+ } else {
299
+ text += char
300
+ index += 1
301
+ }
302
+ }
303
+ return JSON.parse(text)
186
304
  }
187
305
 
188
306
  export function detectBundler(manifest) {
@@ -262,10 +380,17 @@ function printResult(result, io) {
262
380
  const location = relative(process.cwd(), result.root) || '.'
263
381
  io.log(result.mode === 'create' ? `Created a tt project in ${location}.` : `Added tt to ${location}.`)
264
382
  if (result.files.length) io.log(`Generated: ${result.files.join(', ')}`)
383
+ for (const { name, from, to } of result.updated ?? []) {
384
+ io.log(`Updated ${name} from ${from} to ${to}: tt's content mapper needs this TypeScript 7.1 build.`)
385
+ }
265
386
  if (result.manualModule) {
266
387
  io.log(`Add tt() from ${result.manualModule} to your esbuild plugins array.`)
267
388
  }
268
- io.log(result.mode === 'create' ? `Run: cd ${location} && ${runScript(result.packageManager, 'dev')}` : `Run: ${runScript(result.packageManager, 'tt:check')}`)
389
+ io.log(result.mode === 'create' ? `Run: cd ${shellQuote(location)} && ${runScript(result.packageManager, 'dev')}` : `Run: ${runScript(result.packageManager, 'tt:check')}`)
390
+ }
391
+
392
+ export function shellQuote(word) {
393
+ return /^[A-Za-z0-9_@%+=:,.\/-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`
269
394
  }
270
395
 
271
396
  function runScript(packageManager, script) {
@@ -316,8 +441,12 @@ function indentation(source) {
316
441
  return source.match(/\n([ \t]+)\S/)?.[1] ?? ' '
317
442
  }
318
443
 
319
- async function writeJson(path, value, space = ' ') {
320
- await writeFile(path, `${JSON.stringify(value, null, space)}\n`)
444
+ function jsonText(value, space = ' ') {
445
+ return `${JSON.stringify(value, null, space)}\n`
446
+ }
447
+
448
+ async function writeJson(path, value, space) {
449
+ await writeFile(path, jsonText(value, space))
321
450
  }
322
451
 
323
452
  const viteConfig = `import { defineConfig } from 'vite'