@openload28/create-tt 1.0.0-dev.20260930.435 → 1.0.0-dev.20261014.555
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 +13 -1
- package/package.json +1 -1
- package/src/installer.js +153 -34
package/README.md
CHANGED
|
@@ -24,13 +24,25 @@ the existing TypeScript config and declares the content mapper. A config with
|
|
|
24
24
|
project `references` (such as Vite's solution-style `tsconfig.json`) gets a
|
|
25
25
|
`*.tt.json` counterpart for every referenced config inside the project, the
|
|
26
26
|
generated configs reference each other the way the originals do, and the
|
|
27
|
-
generated scripts check them with `tsc -b`.
|
|
27
|
+
generated scripts check them with `tsc -b`. TypeScript does not let a
|
|
28
|
+
referenced project disable emit (TS6310), so a generated config that another
|
|
29
|
+
compiled config references emits declarations only, into
|
|
30
|
+
`node_modules/.cache/tt/`, instead of setting `noEmit`; every other generated
|
|
31
|
+
config sets `noEmit`. Without a bundler, the generated `tt:build` runs
|
|
32
|
+
`ttc -o .tt-build` over the source roots the configurations include (the
|
|
33
|
+
directory part of each `include` pattern before its first wildcard, and each
|
|
34
|
+
`files` entry); several roots each keep their own path under `.tt-build`, so
|
|
35
|
+
relative imports between them still resolve. The initializer sets `typescript` to
|
|
28
36
|
the TypeScript 7.1 build tt is verified with; when the project named another
|
|
29
37
|
version, it replaces it and prints the change. Existing scripts and config files
|
|
30
38
|
stay intact. Re-running init accepts unchanged generated configs;
|
|
31
39
|
if a generated config has been customized, init stops before writing any project
|
|
32
40
|
files.
|
|
33
41
|
Use `--no-install` in CI or when dependencies will be installed later.
|
|
42
|
+
The package name is the directory name made valid for a new npm package
|
|
43
|
+
(lowercase, no leading `.` or `_`, at most 214 characters, and `my-tt-app` in
|
|
44
|
+
place of a reserved or core-module name). After `--`, every argument is the
|
|
45
|
+
directory, even one that starts with `-`.
|
|
34
46
|
New projects use Bun for dependency installation and scripts. Existing projects
|
|
35
47
|
keep the package manager declared in `package.json` or selected by their lockfile.
|
|
36
48
|
|
package/package.json
CHANGED
package/src/installer.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import { lstat, mkdir, readFile, readdir, readlink, realpath, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { builtinModules } from 'node:module'
|
|
5
6
|
|
|
6
7
|
const ownManifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
|
|
7
8
|
const ttChannel = dependencyChannel(ownManifest.version)
|
|
@@ -158,9 +159,9 @@ export async function initializeExisting(options) {
|
|
|
158
159
|
const typeConfig = 'tsconfig.tt.json'
|
|
159
160
|
const generated = []
|
|
160
161
|
const realRoot = await realpath(root)
|
|
161
|
-
const references = existsSync(join(root, 'tsconfig.json'))
|
|
162
|
+
const { references, roots } = existsSync(join(root, 'tsconfig.json'))
|
|
162
163
|
? await typeConfigGraph(realRoot, await projectConfig(realRoot, join(root, 'tsconfig.json')), generated)
|
|
163
|
-
: (
|
|
164
|
+
: defaultTypeConfig(realRoot, typeConfig, generated)
|
|
164
165
|
const typeCheck = `tsc ${references ? '-b' : '-p'} ${typeConfig} --runExternalCode`
|
|
165
166
|
manifest.scripts['tt:check'] ??= typeCheck
|
|
166
167
|
|
|
@@ -180,7 +181,7 @@ export async function initializeExisting(options) {
|
|
|
180
181
|
manifest.scripts['tt:build'] ??= `${typeCheck} && ${adapter.commands.build} --config ${adapter.wrapper}`
|
|
181
182
|
}
|
|
182
183
|
} else {
|
|
183
|
-
manifest.scripts['tt:build'] ??=
|
|
184
|
+
manifest.scripts['tt:build'] ??= buildScript(roots)
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
// Validate the complete output set before changing any project files.
|
|
@@ -199,36 +200,142 @@ export async function initializeExisting(options) {
|
|
|
199
200
|
return { root, packageManager, mode: 'init', bundler: bundler ?? 'none', files, manualModule, updated }
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
async function typeConfigGraph(root, configPath, generated
|
|
203
|
-
|
|
203
|
+
async function typeConfigGraph(root, configPath, generated) {
|
|
204
|
+
const projects = new Map()
|
|
205
|
+
await readConfigGraph(root, configPath, projects)
|
|
206
|
+
const emitting = new Set(
|
|
207
|
+
[...projects.values()]
|
|
208
|
+
.filter((project) => !solutionStyle(project.config))
|
|
209
|
+
.flatMap((project) => project.references.map((reference) => reference.target).filter(Boolean)),
|
|
210
|
+
)
|
|
211
|
+
for (const [path, { config, references }] of projects) {
|
|
212
|
+
const directory = dirname(path)
|
|
213
|
+
const counterpart = typeConfigPath(path)
|
|
214
|
+
const content = tsconfig(
|
|
215
|
+
`./${basename(path)}`,
|
|
216
|
+
emitting.has(path) ? declarationOutput(root, counterpart, config) : undefined,
|
|
217
|
+
)
|
|
218
|
+
if (Array.isArray(config.references)) {
|
|
219
|
+
content.references = references.map(({ reference, target }) =>
|
|
220
|
+
target ? { ...reference, path: relativePath(directory, typeConfigPath(target)) } : reference)
|
|
221
|
+
}
|
|
222
|
+
generated.push([relative(root, counterpart), `${JSON.stringify(content, null, ' ')}\n`])
|
|
223
|
+
}
|
|
224
|
+
const inputs = []
|
|
225
|
+
for (const [path, { config }] of projects) {
|
|
226
|
+
if (!solutionStyle(config)) inputs.push(...await configInputs(path, config))
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
references: Array.isArray(projects.get(configPath).config.references),
|
|
230
|
+
roots: sourceRoots(root, inputs),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function defaultTypeConfig(root, typeConfig, generated) {
|
|
235
|
+
const config = tsconfig()
|
|
236
|
+
generated.push([typeConfig, `${JSON.stringify(config, null, ' ')}\n`])
|
|
237
|
+
return {
|
|
238
|
+
references: false,
|
|
239
|
+
roots: sourceRoots(root, config.include.map((entry) => inputBase(root, entry))),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function configInputs(path, config) {
|
|
244
|
+
const { files, include } = await declaredInputs(path, config, new Set())
|
|
245
|
+
if (!files && !include) return [dirname(path)]
|
|
246
|
+
return [...(files ?? []), ...(include ?? [])]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function declaredInputs(path, config, seen) {
|
|
250
|
+
seen.add(path)
|
|
251
|
+
const directory = dirname(path)
|
|
252
|
+
const own = (key) => Array.isArray(config[key])
|
|
253
|
+
? config[key].filter((entry) => typeof entry === 'string').map((entry) => inputBase(directory, entry))
|
|
254
|
+
: undefined
|
|
255
|
+
let files = own('files')
|
|
256
|
+
let include = own('include')
|
|
257
|
+
const bases = (Array.isArray(config.extends) ? config.extends : [config.extends])
|
|
258
|
+
.filter((base) => typeof base === 'string' && (base.startsWith('./') || base.startsWith('../') || isAbsolute(base)))
|
|
259
|
+
.map((base) => resolve(directory, base))
|
|
260
|
+
.map((base) => (existsSync(base) || base.endsWith('.json') ? base : `${base}.json`))
|
|
261
|
+
.reverse()
|
|
262
|
+
for (const base of bases) {
|
|
263
|
+
if ((files && include) || seen.has(base) || !existsSync(base)) continue
|
|
264
|
+
let inherited
|
|
265
|
+
try {
|
|
266
|
+
inherited = parseJsonc(await readFile(base, 'utf8'))
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new Error(`cannot read ${base}: ${error.message}`)
|
|
269
|
+
}
|
|
270
|
+
const from = await declaredInputs(base, inherited, seen)
|
|
271
|
+
files ??= from.files
|
|
272
|
+
include ??= from.include
|
|
273
|
+
}
|
|
274
|
+
return { files, include }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function inputBase(directory, entry) {
|
|
278
|
+
const segments = entry.split(/[\\/]/)
|
|
279
|
+
const wildcard = segments.findIndex((segment) => /[*?]/.test(segment))
|
|
280
|
+
return resolve(directory, ...(wildcard === -1 ? segments : segments.slice(0, wildcard)))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sourceRoots(root, inputs) {
|
|
284
|
+
const unique = [...new Set(inputs)].filter((input) => input === root || insideRoot(root, input))
|
|
285
|
+
const existing = unique.filter((input) => existsSync(input))
|
|
286
|
+
const chosen = existing.length > 0 ? existing : unique
|
|
287
|
+
const posix = (path) => relative(root, path).split('\\').join('/') || '.'
|
|
288
|
+
return chosen
|
|
289
|
+
.filter((input) => !chosen.some((other) => other !== input && (other === root || insideRoot(other, input))))
|
|
290
|
+
.map((input) => ({
|
|
291
|
+
input: posix(input),
|
|
292
|
+
mirror: posix(existsSync(input) && !statSync(input).isDirectory() ? dirname(input) : input),
|
|
293
|
+
}))
|
|
294
|
+
.sort((left, right) => (left.input < right.input ? -1 : left.input > right.input ? 1 : 0))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildScript(roots) {
|
|
298
|
+
if (roots.length === 1) return `ttc -o .tt-build ${shellQuote(roots[0].input)}`
|
|
299
|
+
return roots
|
|
300
|
+
.map(({ input, mirror }) => `ttc -o ${shellQuote(mirror === '.' ? '.tt-build' : `.tt-build/${mirror}`)} ${shellQuote(input)}`)
|
|
301
|
+
.join(' && ')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function readConfigGraph(root, configPath, projects) {
|
|
204
305
|
let config
|
|
205
306
|
try {
|
|
206
307
|
config = parseJsonc(await readFile(configPath, 'utf8'))
|
|
207
308
|
} catch (error) {
|
|
208
309
|
throw new Error(`cannot read ${configPath}: ${error.message}`)
|
|
209
310
|
}
|
|
311
|
+
const project = { config, references: [] }
|
|
312
|
+
projects.set(configPath, project)
|
|
210
313
|
const directory = dirname(configPath)
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
314
|
+
for (const reference of Array.isArray(config.references) ? config.references : []) {
|
|
315
|
+
const lexical = typeof reference?.path === 'string' && referencedConfig(directory, reference.path)
|
|
316
|
+
const resolved = lexical && existsSync(lexical) && await realpath(lexical)
|
|
317
|
+
const target = resolved && insideRoot(root, resolved) ? resolved : undefined
|
|
318
|
+
project.references.push({ reference, target })
|
|
319
|
+
if (target && !projects.has(target)) await readConfigGraph(root, target, projects)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function typeConfigPath(configPath) {
|
|
324
|
+
return configPath.replace(/\.json$/, '') + '.tt.json'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function solutionStyle(config) {
|
|
328
|
+
return Array.isArray(config.files) && config.files.length === 0 && config.include === undefined
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function declarationOutput(root, counterpart, config) {
|
|
332
|
+
const directory = dirname(counterpart)
|
|
333
|
+
const cache = relativePath(directory, join(root, 'node_modules', '.cache', 'tt', relative(root, directory)))
|
|
334
|
+
const options = { noEmit: false, emitDeclarationOnly: true, outDir: cache, declarationDir: cache }
|
|
335
|
+
if (config.compilerOptions?.tsBuildInfoFile !== undefined) {
|
|
336
|
+
options.tsBuildInfoFile = `${cache}/${basename(counterpart, '.json')}.tsbuildinfo`
|
|
229
337
|
}
|
|
230
|
-
|
|
231
|
-
return hasReferences
|
|
338
|
+
return options
|
|
232
339
|
}
|
|
233
340
|
|
|
234
341
|
async function projectConfig(root, path) {
|
|
@@ -316,14 +423,18 @@ function parseArguments(argv) {
|
|
|
316
423
|
install: true,
|
|
317
424
|
bundler: 'auto',
|
|
318
425
|
}
|
|
426
|
+
let positional = false
|
|
319
427
|
while (args.length) {
|
|
320
428
|
const arg = args.shift()
|
|
321
|
-
if (
|
|
429
|
+
if (positional || !arg.startsWith('-')) {
|
|
430
|
+
if (options.directory) throw new Error(`unknown argument: ${arg}`)
|
|
431
|
+
options.directory = arg
|
|
432
|
+
} else if (arg === '--') positional = true
|
|
433
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
322
434
|
else if (arg === '--no-install') options.install = false
|
|
323
435
|
else if (arg === '--bundler') options.bundler = requiredValue(arg, args)
|
|
324
436
|
else if (arg === '--package-manager') options.packageManager = requiredValue(arg, args)
|
|
325
437
|
else if (arg === '--registry') options.registry = registryUrl(requiredValue(arg, args))
|
|
326
|
-
else if (!arg.startsWith('-') && !options.directory) options.directory = arg
|
|
327
438
|
else throw new Error(`unknown argument: ${arg}`)
|
|
328
439
|
}
|
|
329
440
|
if (!['auto', 'none', ...Object.keys(bundlers)].includes(options.bundler)) {
|
|
@@ -410,10 +521,10 @@ export default typeof base === 'function'
|
|
|
410
521
|
`
|
|
411
522
|
}
|
|
412
523
|
|
|
413
|
-
function tsconfig(extendsConfig) {
|
|
524
|
+
function tsconfig(extendsConfig, compilerOptions = { noEmit: true }) {
|
|
414
525
|
const config = {
|
|
415
526
|
...(extendsConfig
|
|
416
|
-
? { extends: extendsConfig, compilerOptions
|
|
527
|
+
? { extends: extendsConfig, compilerOptions }
|
|
417
528
|
: {
|
|
418
529
|
compilerOptions: {
|
|
419
530
|
target: 'ES2022',
|
|
@@ -432,9 +543,16 @@ function tsconfig(extendsConfig) {
|
|
|
432
543
|
return config
|
|
433
544
|
}
|
|
434
545
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
546
|
+
const RESERVED_NAMES = new Set(['node_modules', 'favicon.ico', ...builtinModules])
|
|
547
|
+
|
|
548
|
+
export function packageName(value) {
|
|
549
|
+
const normalized = value
|
|
550
|
+
.toLowerCase()
|
|
551
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
552
|
+
.replace(/^[._-]+/, '')
|
|
553
|
+
.slice(0, 214)
|
|
554
|
+
.replace(/-+$/, '')
|
|
555
|
+
return normalized === '' || RESERVED_NAMES.has(normalized) ? 'my-tt-app' : normalized
|
|
438
556
|
}
|
|
439
557
|
|
|
440
558
|
function indentation(source) {
|
|
@@ -487,4 +605,5 @@ Options:
|
|
|
487
605
|
--package-manager <npm|pnpm|yarn|bun>
|
|
488
606
|
--registry <url> install from an npm-compatible private/local registry
|
|
489
607
|
--no-install
|
|
490
|
-
-h, --help
|
|
608
|
+
-h, --help
|
|
609
|
+
-- every argument after it is the directory, even one starting with -`
|