UNPKG

ttsc

Version:

General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.

293 lines 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseFlags = parseFlags; exports.getBoolean = getBoolean; exports.getString = getString; exports.getStringList = getStringList; exports.getNumber = getNumber; const schema_1 = require("./schema"); /** * Parse `argv` according to FLAG_SCHEMA filtered by `subcommand`. Returns a * `ParseResult`. Throws `Error` (with the configured prefix) on invalid input — * unknown subcommand-only flag, missing required value, value that fails its * validator. */ function parseFlags(opts) { const accepted = new Map(); for (const flag of (0, schema_1.flagsForSubcommand)(opts.subcommand)) { // A flag enters the launcher's `accepted` set only when its // `consumedBy` includes `"launcher"`. Flags consumed solely by tsgo // or by native sidecars (e.g. `--showConfig`, `--listFilesOnly`) // must fall through to `forwardKnownButUnaccepted` so the launcher // forwards them verbatim instead of storing them in `values` where // no consumer reads them back out. The previous shape — filter on // `subcommands` only — silently dropped every tsgo-only terminal // flag at the launcher boundary (the RC-1 / RC-2 class the schema // is meant to make impossible). if (!flag.consumedBy.includes("launcher")) continue; accepted.set((0, schema_1.normalizeFlagToken)(flag.name), flag); for (const alias of flag.aliases ?? []) { accepted.set((0, schema_1.normalizeFlagToken)(alias), flag); } } const values = new Map(); const repeated = new Map(); const passthrough = []; const positional = []; const tail = []; let remainder = null; if (opts.honorDoubleDashSeparator === true) { const separator = opts.argv.indexOf("--"); if (separator !== -1) { remainder = [...opts.argv.slice(separator + 1)]; } } const head = [ ...(remainder === null ? opts.argv : opts.argv.slice(0, indexOfSeparator(opts.argv))), ]; let forwardingTail = false; while (head.length !== 0) { const current = head.shift(); if (forwardingTail) { // Post-sentinel tokens belong to the user's program (e.g. the typia.ts // entry's own argv: `ttsx typia.ts generate --input X`). They MUST NOT // be forwarded to tsgo — the caller distinguishes `tail` from // `passthrough` so script args never reach tsgo's option parser. tail.push(current); continue; } // Only a `-`-prefixed token can name a flag. Bare tokens are input files // and flag values; resolving one against the schema would let the `all` of // `--target all` masquerade as `--all` now that the lookup is dash- and // case-insensitive. if (current.startsWith("-")) { // Split `--foo=value` / `-p=value` before resolving launcher-owned // options. A tsgo-only option is still forwarded byte-for-byte; pinned // tsgo does not split `=`, so it will reject that spelling itself. const equalsIndex = current.indexOf("="); const token = equalsIndex === -1 ? current : current.slice(0, equalsIndex); const inlineValue = equalsIndex === -1 ? undefined : current.slice(equalsIndex + 1); // Resolution is by flag identity, not by exact spelling: `--NOEMIT` and // `-noEmit` are the same option to the compiler ttsc forwards to, so they // must be the same option here. Otherwise a case variant of a ttsc-owned // flag falls through to the escape hatch below and tsgo honours it while // every ttsc-side consumer stays silent. const flag = accepted.get((0, schema_1.normalizeFlagToken)(token)); if (flag !== undefined) { consumeFlag(values, repeated, flag, token, inlineValue, head, opts.errorPrefix); continue; } // Token IS a known flag but is not accepted by THIS subcommand. The // engine forwards it to tsgo just like an unknown flag — the same // policy the bare-lane parser applied (RC-1 prevention). const globalFlag = (0, schema_1.resolveFlagSpec)(token); if (globalFlag !== undefined) { forwardKnownButUnaccepted(passthrough, globalFlag, current, inlineValue, head, opts.isPositional); continue; } // Truly unknown `-`-prefixed token: forward to tsgo verbatim. This // is what makes `ttsc --strict file.ts` work — ttsc does not need to // re-implement every tsgo flag. passthrough.push(current); continue; } // Bare token: either a genuine positional argument (file / entry / project // path) or the space-separated value of a preceding forwarded flag. When a // caller forwards unknown flags it supplies `isPositional` to tell the two // apart; a forwarded value is appended to `passthrough` in place so the // `--flag value` pair reaches tsgo in its original order and adjacency, // instead of being split into a separate bucket the caller later // concatenates out of order. if (opts.isPositional !== undefined && !opts.isPositional(current)) { passthrough.push(current); continue; } positional.push(current); if (opts.forwardAfterFirstPositional === true && positional.length === 1) { forwardingTail = true; } } if (remainder !== null) { // `--` separator: everything after goes to the user program when we are // in tail mode (ttsx after the entry), otherwise to tsgo as passthrough. const sink = forwardingTail ? tail : passthrough; for (const token of remainder) sink.push(token); } return { values, repeated, passthrough, positional, tail }; } /** * Pull the value for `flag` from either `inlineValue` (`--flag=value`) or the * next argv token (`--flag value`), validate it per the schema, and write it to * the result map. */ function consumeFlag(values, repeated, flag, token, inlineValue, rest, errorPrefix) { const record = (value) => { values.set(flag.name, value); if (flag.repeatable !== true) return; const list = repeated.get(flag.name); if (list === undefined) repeated.set(flag.name, [value]); else list.push(value); }; if (flag.kind === "boolean") { if (inlineValue !== undefined) { // `--flag=false` / `--flag=true` inline form. Anything other than // a recognised literal stays loud: `--singleThreaded=yes` silently // becoming `true` is the kind of footgun the RCA's RC-4 class // covers. Mirrors `validatePositiveInt`'s style. const literal = parseBooleanLiteral(inlineValue); if (literal === undefined) { throw new Error(`${errorPrefix} ${token} expects \`true\` or \`false\`, got ${JSON.stringify(inlineValue)}`); } record(literal); return; } // Space form `--flag true` / `--flag false`: peek the next token and // consume it only when it parses as a boolean literal. tsgo accepts // this shape natively; the launcher must mirror it so `ttsc --noEmit // false` does not corrupt argv (positional sink getting `false`, // tsgo seeing it as a stray input file). if (rest.length > 0) { const peek = parseBooleanLiteral(rest[0]); if (peek !== undefined) { rest.shift(); record(peek); return; } } record(true); return; } const raw = inlineValue !== undefined ? inlineValue : takeValueToken(token, rest, errorPrefix); if (flag.validator === "positiveInt") { record(validatePositiveInt(token, raw, errorPrefix)); return; } record(raw); } /** * Read the value token that follows `flag`. Throws if argv ends here OR if the * next token looks like another flag (`-` prefix). Without the "looks like a * flag" guard `ttsc --cwd --strict src/main.ts` would silently consume * `--strict` as the value of `--cwd`, leaving `--strict` lost and `cwd` set to * a junk path. Mirrors the symmetric guard already in * `forwardKnownButUnaccepted` (RC-1 fairness — the two value-resolution paths * must agree on what counts as "a missing value"). */ function takeValueToken(flag, rest, errorPrefix) { const value = rest.shift(); if (value === undefined) { throw new Error(`${errorPrefix} ${flag} requires a value`); } if (value.startsWith("-")) { // Put it back so the next loop iteration parses it as its own flag. rest.unshift(value); throw new Error(`${errorPrefix} ${flag} requires a value (next token ${JSON.stringify(value)} starts with "-")`); } return value; } /** * Parse a CLI boolean literal — `true`/`false` only (case-sensitive to match * tsgo's parser). Returns `undefined` for any other token so the caller knows * to throw or treat as a non-value. */ function parseBooleanLiteral(raw) { if (raw === "true") return true; if (raw === "false") return false; return undefined; } /** * Validate a `positiveInt` value. Mirrors tsgo's `--checkers minValue:1` * constraint so a typo fails loudly at the launcher rather than reaching tsgo * with an invalid argument. */ function validatePositiveInt(flag, raw, errorPrefix) { const value = Number(raw); if (!Number.isInteger(value) || value < 1) { throw new Error(`${errorPrefix} ${flag} expects a positive integer, got ${JSON.stringify(raw)}`); } return value; } /** * Forward a flag the schema knows about but the current subcommand does not * accept. The launcher will hand it to tsgo (or to native sidecars via * `--tsgo-args`); without this branch the parser would lose the value token of * a `--flag value` pair. * * A value option owned by tsgo always consumes its next bare token, even when * it ends in `.ts`; `--rootDir src.ts main.ts` has one option value and one * source. Schema rows belonging only to another ttsc layer still consult * `isPositional`, because tsgo does not own their arity. */ function forwardKnownButUnaccepted(passthrough, flag, original, inlineValue, rest, isPositional) { passthrough.push(original); // Boolean flags carry no required value. `--foo=value` is already one token. if (flag.kind === "boolean" || inlineValue !== undefined) { return; } if (rest.length === 0) return; if (rest[0].startsWith("-")) return; const tsgoOwnsArity = flag.consumedBy.includes("tsgo") || flag.forwardTo === "tsgo"; if (tsgoOwnsArity === false && isPositional !== undefined && isPositional(rest[0])) { return; } passthrough.push(rest.shift()); } /** * Return the index of the `--` separator in `argv`, or `argv.length` when * absent. The parser slices on this index when honorDoubleDashSeparator. */ function indexOfSeparator(argv) { const index = argv.indexOf("--"); return index === -1 ? argv.length : index; } // ----------------------------------------------------------------------------- // Typed accessors. The parser stores raw values (string | boolean | number) so // the engine stays untyped; callers pick the shape per flag. // ----------------------------------------------------------------------------- /** Return the boolean value of `flag` or `undefined` if not present. */ function getBoolean(result, flag) { const value = result.values.get(flag); if (typeof value === "boolean") return value; return undefined; } /** Return the string value of `flag` or `undefined` if not present. */ function getString(result, flag) { const value = result.values.get(flag); if (typeof value === "string") return value; return undefined; } /** * Return every string value accepted for a `repeatable` flag, in argv order. * * `values` keeps only the last occurrence, which is the wrong answer for a flag * whose whole point is repetition (`ttsx -r a -r b` preloads both). Returns an * empty array when the flag never appeared. */ function getStringList(result, flag) { return (result.repeated.get(flag) ?? []).filter((value) => typeof value === "string"); } /** Return the numeric value of `flag` or `undefined` if not present. */ function getNumber(result, flag) { const value = result.values.get(flag); if (typeof value === "number") return value; return undefined; } //# sourceMappingURL=parser.js.map