@openload28/create-tt 1.0.0-dev.20260928.424 → 1.0.0-dev.20260929.434
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 +8 -2
- package/package.json +1 -1
- package/src/installer.js +147 -18
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.
|
|
24
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
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'] ??=
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
files
|
|
185
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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'
|