@orkestrel/scaffold 0.0.48 → 0.0.50

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.
@@ -3,7 +3,7 @@ import { fillTemplate } from "@orkestrel/template";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  var package_default = {
5
5
  name: "@orkestrel/scaffold",
6
- version: "0.0.48",
6
+ version: "0.0.50",
7
7
  description: "Scaffold workspaces with five commands: new, audit, repair, catalog, and overwrite.",
8
8
  keywords: [
9
9
  "audit",
@@ -83,23 +83,24 @@ var package_default = {
83
83
  "build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
84
84
  "build:src:bin": "vite build --config configs/src/vite.bin.config.ts",
85
85
  "build:host": "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"",
86
+ "build:inventory": "node -e \"import('./dist/src/server/index.js').then((m)=>{const p=process.argv[1]??'host.json',n=m.stageInventory(process.cwd(),p).entries.length;console.log('build-inventory: staged '+n+' file(s) into '+p)})\"",
86
87
  "prepack": "npm run build",
87
88
  "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:distribution -- --mode release"
88
89
  },
89
90
  dependencies: {
90
- "@orkestrel/console": "^0.0.9",
91
+ "@orkestrel/console": "^0.0.10",
91
92
  "@orkestrel/contract": "^0.0.13",
92
- "@orkestrel/emitter": "^0.0.7",
93
- "@orkestrel/markdown": "^0.0.9",
93
+ "@orkestrel/emitter": "^0.0.8",
94
+ "@orkestrel/markdown": "^0.0.10",
94
95
  "@orkestrel/process": "^0.0.6",
95
- "@orkestrel/template": "^0.0.4"
96
+ "@orkestrel/template": "^0.0.5"
96
97
  },
97
98
  devDependencies: {
98
99
  "@microsoft/api-extractor": "^7.59.0",
99
- "@orkestrel/guide": "^0.0.12",
100
- "@orkestrel/html": "^0.0.4",
101
- "@orkestrel/probe": "^0.0.2",
102
- "@orkestrel/test": "^0.0.9",
100
+ "@orkestrel/guide": "^0.0.13",
101
+ "@orkestrel/html": "^0.0.5",
102
+ "@orkestrel/probe": "^0.0.3",
103
+ "@orkestrel/test": "^0.0.11",
103
104
  "@types/node": "^26.2.0",
104
105
  "@vitest/browser-playwright": "^4.1.11",
105
106
  "oxfmt": "^0.64.0",
@@ -251,6 +252,8 @@ var HOST_PATHS = Object.freeze([
251
252
  "guides/guide.md",
252
253
  "guides/scaffold.md"
253
254
  ]);
255
+ /** The repository-relative path where the committed vendored-file inventory is served. */
256
+ var HOST_INVENTORY_PATH = "host.json";
254
257
  /**
255
258
  * The vendored paths whose present bytes belong to each workspace, frozen.
256
259
  *
@@ -321,10 +324,21 @@ var SERVICE_SCRIPT_PATH = "scripts/service.sh";
321
324
  var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
322
325
  /** The guide-parity proof whose presence selects the planned `guides` project. */
323
326
  var GUIDES_TEST_PATH = "tests/guides.test.ts";
324
- /** The packed-package proof whose presence makes a workspace `distribution`. */
327
+ /** The generated packed-package proof every publishing workspace is planned at. */
325
328
  var DISTRIBUTION_TEST_PATH = "tests/distribution.test.ts";
329
+ /**
330
+ * The `prepublishOnly` row that runs the packed-package proof against a real registry.
331
+ *
332
+ * @remarks
333
+ * The proof reads `import.meta.env.MODE`, so without `--mode release` it passes
334
+ * on an unreachable registry instead of failing. The row therefore has one home
335
+ * and both the script compiler and the manifest region writer read it from here.
336
+ */
337
+ var RELEASE_PROOF_COMMAND = "npm run test:distribution -- --mode release";
326
338
  /** The cross-environment composition proof whose presence makes a workspace `integration`. */
327
339
  var INTEGRATION_TEST_PATH = "tests/integration.test.ts";
340
+ /** The manifest path every compiler plan emits with birth ownership. */
341
+ var MANIFEST_PATH = "package.json";
328
342
  /** The official-tooling drift proof whose presence makes a workspace `conformance`. */
329
343
  var CONFORMANCE_TEST_PATH = "tests/conformance.test.ts";
330
344
  /** The live-service readiness module whose presence makes a workspace `service`. */
@@ -399,6 +413,8 @@ var MAX_NAME_LENGTH = 203;
399
413
  var MAX_DEPENDENCY_NAME_LENGTH = 214;
400
414
  /** Maximum length of one declared package range. */
401
415
  var MAX_RANGE_LENGTH = 2048;
416
+ /** Maximum length of one manifest script name or command. */
417
+ var MAX_SCRIPT_LENGTH = 4096;
402
418
  /** Maximum length of one path, matching the longest a supported filesystem accepts. */
403
419
  var MAX_PATH_LENGTH = 32767;
404
420
  /** Maximum items accepted in one public collection. */
@@ -768,8 +784,8 @@ const resolve = {
768
784
  }
769
785
  }
770
786
 
771
- export function appBrowser(): UserConfig {
772
- return applicationBrowser(false)
787
+ export function appBrowser(options?: UserConfig): UserConfig {
788
+ return mergeConfig(applicationBrowser(false), options ?? {})
773
789
  }
774
790
  {{showcaseFactory}}`,
775
791
  server: `export const appServer = (options?: UserConfig): UserConfig =>
@@ -1587,6 +1603,865 @@ describe('bin entry', () => {
1587
1603
  })
1588
1604
  })
1589
1605
  `,
1606
+ distribution: Object.freeze({
1607
+ proof: `// The artifact a consumer installs, measured rather than described. This workspace
1608
+ // is packed and installed into a throwaway consumer, and every following claim is read
1609
+ // off that installed tree: the exports map it publishes, the declarations it ships,
1610
+ // and the module objects a real runtime hands a consumer. Nothing here names this
1611
+ // package, one of its exports, or how many there are, so the proof stays true as
1612
+ // the published surface moves.
1613
+ {{types}}import type { SpawnSyncReturns } from 'node:child_process'
1614
+ import type { TestContext } from 'vitest'
1615
+ import { spawnSync } from 'node:child_process'
1616
+ import {
1617
+ existsSync,
1618
+ mkdirSync,
1619
+ mkdtempSync,
1620
+ readdirSync,
1621
+ readFileSync,
1622
+ rmSync,
1623
+ statSync,
1624
+ writeFileSync,
1625
+ } from 'node:fs'
1626
+ {{transport}}import { tmpdir } from 'node:os'
1627
+ import { dirname, join, resolve } from 'node:path'
1628
+ import { fileURLToPath } from 'node:url'
1629
+ {{launcher}}import ts from 'typescript'
1630
+ import { afterAll, describe, expect, it } from 'vitest'
1631
+
1632
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
1633
+ const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm'
1634
+ // Windows needs a shell to launch a \`.cmd\`: Node refuses one directly since the
1635
+ // batch-argument hardening, and \`spawnSync\` returns \`EINVAL\` with a null status
1636
+ // rather than an exit code a caller can read. Every following argument is a literal or
1637
+ // a path this file built, so the shell has nothing to escape.
1638
+ const SHELL = process.platform === 'win32'
1639
+ // \`prepublishOnly\` runs this proof as \`npm run test:distribution -- --mode release\`.
1640
+ // Release is the publish gate, so evidence it cannot obtain fails there and skips
1641
+ // everywhere else: a gate that passes on missing evidence proves nothing.
1642
+ const RELEASE = import.meta.env.MODE === 'release'
1643
+ // The built output directory a browser face is published from. Every selection here
1644
+ // reads this prefix off the export TARGET and never off the subpath name. A
1645
+ // workspace whose only published face is the browser one publishes that face at the
1646
+ // root subpath, so a rule keyed on the subpath name drives a browser bundle through
1647
+ // Node and the miss is silent.
1648
+ const BROWSER_OUTPUT = './dist/src/browser/'
1649
+ const ABSENT_SUBPATH = '/no-subpath-is-published-under-this-name'
1650
+ const PING = ['ping', '--fetch-retries=0', '--fetch-timeout=5000', '--loglevel=silent']
1651
+ const ESM_DRIVER = 'drive.mjs'
1652
+ const CJS_DRIVER = 'drive.cjs'
1653
+ const CONSUMER_MANIFEST = \`{ "name": "distribution-consumer", "private": true, "type": "module" }\\n\`
1654
+ const ESM_DRIVER_SOURCE = \`const entry = await import(process.argv[2])
1655
+ process.stdout.write(JSON.stringify(Object.keys(entry).sort()))
1656
+ \`
1657
+ const CJS_DRIVER_SOURCE = \`const entry = require(process.argv[2])
1658
+ process.stdout.write(JSON.stringify(Object.keys(entry).sort()))
1659
+ \`
1660
+
1661
+ // The extensions a JavaScript handler loads as modules. Node loads a native addon
1662
+ // through its addon handler instead, so that extension is named separately.
1663
+ const MODULE_EXTENSIONS = ['.js', '.mjs', '.cjs']
1664
+ const ADDON_EXTENSION = '.node'
1665
+ // The extensions a declaration file carries. A \`require\` condition declares
1666
+ // \`.d.cts\` and an ESM-only one \`.d.mts\`, so the \`.d.ts\` spelling alone does not
1667
+ // name them.
1668
+ const DECLARATION_EXTENSIONS = ['.d.ts', '.d.cts', '.d.mts']
1669
+ type Format = 'module' | 'commonjs'
1670
+
1671
+ // The Node import target is resolved with the conditions that driver supplies. The
1672
+ // CommonJS compile probe is selected from its declaration's format, and its runtime
1673
+ // drive loads the same subpath through Node's require resolver. Vite's production
1674
+ // client build enables its module and browser conditions.
1675
+ const RUNTIME_CONDITIONS = Object.freeze({
1676
+ module: Object.freeze(['node-addons', 'node', 'import', 'module-sync']),
1677
+ commonjs: Object.freeze(['node-addons', 'node', 'require', 'module-sync']),
1678
+ browser: Object.freeze(['module', 'browser', 'production', 'import']),
1679
+ })
1680
+ // TypeScript's Node resolutions add \`node\` to the format condition. Its bundler
1681
+ // resolution does not, so a browser drive compares against the declaration a bundler
1682
+ // consumer reads rather than borrowing the Node declaration.
1683
+ const BUNDLER_CONDITIONS = Object.freeze({
1684
+ module: ['types', 'import'],
1685
+ commonjs: ['types', 'require'],
1686
+ })
1687
+ const DECLARATION_CONDITIONS = Object.freeze({
1688
+ module: ['types', 'node', 'import'],
1689
+ commonjs: ['types', 'node', 'require'],
1690
+ browser: BUNDLER_CONDITIONS.module,
1691
+ })
1692
+
1693
+ interface Resolution {
1694
+ readonly label: string
1695
+ readonly resolution: ts.ModuleResolutionKind
1696
+ readonly module: ts.ModuleKind
1697
+ readonly conditions: Readonly<Record<Format, readonly string[]>>
1698
+ }
1699
+
1700
+ interface TargetResolution {
1701
+ readonly target: string
1702
+ }
1703
+
1704
+ // Each compile driver carries the conditions TypeScript applies for its resolution
1705
+ // and importing format. A \`require\`-only subpath therefore stays in each CommonJS
1706
+ // probe that can resolve it.
1707
+ const RESOLUTIONS: readonly Resolution[] = [
1708
+ {
1709
+ label: 'node16',
1710
+ resolution: ts.ModuleResolutionKind.Node16,
1711
+ module: ts.ModuleKind.Node16,
1712
+ conditions: DECLARATION_CONDITIONS,
1713
+ },
1714
+ {
1715
+ label: 'nodenext',
1716
+ resolution: ts.ModuleResolutionKind.NodeNext,
1717
+ module: ts.ModuleKind.NodeNext,
1718
+ conditions: DECLARATION_CONDITIONS,
1719
+ },
1720
+ {
1721
+ label: 'bundler',
1722
+ resolution: ts.ModuleResolutionKind.Bundler,
1723
+ module: ts.ModuleKind.ESNext,
1724
+ conditions: BUNDLER_CONDITIONS,
1725
+ },
1726
+ ]
1727
+
1728
+ const FORMATS: ReadonlyArray<readonly [extension: string, format: Format]> = [
1729
+ ['ts', 'module'],
1730
+ ['cts', 'commonjs'],
1731
+ ]
1732
+
1733
+ // One published subpath, resolved to what this proof can drive: the specifier a
1734
+ // consumer writes, the declarations its consumer formats name, whether its target
1735
+ // is a browser bundle, and whether it answers \`import\` and \`require\` at all.
1736
+ interface Entry {
1737
+ readonly subpath: string
1738
+ readonly specifier: string
1739
+ readonly mapping: unknown
1740
+ readonly declaration: {
1741
+ readonly module: string | undefined
1742
+ readonly commonjs: string | undefined
1743
+ readonly browser: string | undefined
1744
+ }
1745
+ readonly browser: boolean
1746
+ readonly module: boolean
1747
+ readonly commonjs: boolean
1748
+ readonly required: boolean
1749
+ }
1750
+
1751
+ // The installed tree every claim is read from. Every subpath the exports map names
1752
+ // lands in exactly one of \`entries\`, \`undeclared\`, and \`excluded\`, so a subpath this
1753
+ // proof cannot drive is reported rather than dropped.
1754
+ interface Stage {
1755
+ readonly consumer: string
1756
+ readonly installed: string
1757
+ readonly archives: readonly string[]
1758
+ readonly entries: readonly Entry[]
1759
+ readonly subpaths: readonly string[]
1760
+ readonly undeclared: readonly string[]
1761
+ readonly excluded: readonly string[]
1762
+ readonly targets: readonly string[]
1763
+ }
1764
+
1765
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
1766
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1767
+ }
1768
+
1769
+ function isNames(value: unknown): value is readonly string[] {
1770
+ return Array.isArray(value) && value.every((name) => typeof name === 'string')
1771
+ }
1772
+
1773
+ // A fallback list, which is what Node reads an array in an exports entry as. The
1774
+ // narrowing is what the following walkers need: \`Array.isArray\` widens an \`unknown\`
1775
+ // member to \`any\`, and an entry read that way is not read at all.
1776
+ function isList(value: unknown): value is readonly unknown[] {
1777
+ return Array.isArray(value)
1778
+ }
1779
+
1780
+ // Whether a string is a valid package target. Node rejects a target outside the
1781
+ // package and a target containing a dot, parent, or node_modules segment during
1782
+ // package-target resolution. A later module-resolution failure is not the same
1783
+ // thing: an array falls through the former and keeps the latter.
1784
+ function isPackageTarget(target: string): boolean {
1785
+ if (!target.startsWith('./')) return false
1786
+ for (const segment of target.slice(2).split(/[\\\\/]/u)) {
1787
+ let decoded = segment
1788
+ try {
1789
+ decoded = decodeURIComponent(segment)
1790
+ } catch {}
1791
+ const normalized = decoded.toLowerCase()
1792
+ if (normalized === '.' || normalized === '..' || normalized === 'node_modules') return false
1793
+ }
1794
+ return true
1795
+ }
1796
+
1797
+ function readJson(path: string): unknown {
1798
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
1799
+ return parsed
1800
+ }
1801
+
1802
+ function readManifestName(path: string): string {
1803
+ const manifest = readJson(path)
1804
+ if (!isRecord(manifest) || typeof manifest.name !== 'string') {
1805
+ throw new Error(\`The manifest at \${path} declares no package name\`)
1806
+ }
1807
+ return manifest.name
1808
+ }
1809
+
1810
+ function writeFile(path: string, content: string): void {
1811
+ mkdirSync(dirname(path), { recursive: true })
1812
+ writeFileSync(path, content)
1813
+ }
1814
+
1815
+ function readOutput(result: SpawnSyncReturns<string>): string {
1816
+ return \`\${result.stdout ?? ''}\${result.stderr ?? ''}\`.trim()
1817
+ }
1818
+
1819
+ function runNpm(args: readonly string[], cwd: string): SpawnSyncReturns<string> {
1820
+ return spawnSync(NPM, [...args], {
1821
+ cwd,
1822
+ encoding: 'utf8',
1823
+ env: { ...process.env, npm_config_cache: CACHE },
1824
+ shell: SHELL,
1825
+ windowsHide: true,
1826
+ })
1827
+ }
1828
+
1829
+ function runNode(args: readonly string[], cwd: string): SpawnSyncReturns<string> {
1830
+ return spawnSync(process.execPath, [...args], { cwd, encoding: 'utf8', windowsHide: true })
1831
+ }
1832
+
1833
+ // Node's own condition matching, read in declaration order.
1834
+ function resolvePackageTarget(
1835
+ entry: unknown,
1836
+ conditions: readonly string[],
1837
+ ): TargetResolution | undefined {
1838
+ if (typeof entry === 'string') return { target: entry }
1839
+ if (isList(entry)) {
1840
+ for (const member of entry) {
1841
+ const resolved = resolvePackageTarget(member, conditions)
1842
+ if (resolved !== undefined && isPackageTarget(resolved.target)) return resolved
1843
+ }
1844
+ return undefined
1845
+ }
1846
+ if (!isRecord(entry)) return undefined
1847
+ for (const [condition, nested] of Object.entries(entry)) {
1848
+ if (condition !== 'default' && !conditions.includes(condition)) continue
1849
+ const resolved = resolvePackageTarget(nested, conditions)
1850
+ if (resolved !== undefined) return resolved
1851
+ }
1852
+ return undefined
1853
+ }
1854
+
1855
+ // A flat entry, a condition-nested entry, and a fallback list all resolve through
1856
+ // one walker. An entry may declare \`types\` beside \`default\` at its top level
1857
+ // rather than inside \`import\`, so a fixed \`entry.import.types\` lookup is not
1858
+ // equivalent to condition resolution.
1859
+ function resolveTarget(entry: unknown, conditions: readonly string[]): string | undefined {
1860
+ return resolvePackageTarget(entry, conditions)?.target
1861
+ }
1862
+
1863
+ // Whether a path is a physical file. TypeScript's file-existence check refuses a
1864
+ // directory at the same spelling and continues to the outer package scope.
1865
+ function matchesFile(path: string): boolean {
1866
+ try {
1867
+ return statSync(path).isFile()
1868
+ } catch {
1869
+ return false
1870
+ }
1871
+ }
1872
+
1873
+ // TypeScript resolves a declaration target by accepting an existing declaration
1874
+ // directly or by substituting beside a JavaScript target. A missing target leaves
1875
+ // the containing condition or fallback list unresolved, so the walk continues.
1876
+ function targetToDeclaration(target: string, installed: string): string | undefined {
1877
+ if (!isPackageTarget(target)) return undefined
1878
+ let declaration = target
1879
+ if (target.endsWith('.cjs')) declaration = \`\${target.slice(0, -4)}.d.cts\`
1880
+ else if (target.endsWith('.mjs')) declaration = \`\${target.slice(0, -4)}.d.mts\`
1881
+ else if (target.endsWith('.js')) declaration = \`\${target.slice(0, -3)}.d.ts\`
1882
+ else if (!isDeclaration(target)) return undefined
1883
+ return matchesFile(join(installed, declaration)) ? declaration : undefined
1884
+ }
1885
+
1886
+ // The declaration TypeScript resolves through one importing format's conditions.
1887
+ // Condition objects keep manifest order, and arrays keep fallback order.
1888
+ function resolveDeclaration(
1889
+ entry: unknown,
1890
+ conditions: readonly string[],
1891
+ installed: string,
1892
+ ): string | undefined {
1893
+ if (typeof entry === 'string') return targetToDeclaration(entry, installed)
1894
+ if (isList(entry)) {
1895
+ for (const member of entry) {
1896
+ const resolved = resolveDeclaration(member, conditions, installed)
1897
+ if (resolved !== undefined) return resolved
1898
+ }
1899
+ return undefined
1900
+ }
1901
+ if (!isRecord(entry)) return undefined
1902
+ for (const [condition, nested] of Object.entries(entry)) {
1903
+ if (condition !== 'default' && !conditions.includes(condition)) continue
1904
+ const resolved = resolveDeclaration(nested, conditions, installed)
1905
+ if (resolved !== undefined) return resolved
1906
+ }
1907
+ return undefined
1908
+ }
1909
+
1910
+ // The nearest package scope that decides a \`.d.ts\` declaration's module format. A
1911
+ // physical nested manifest starts a scope even when it omits \`type\` or cannot be
1912
+ // parsed. A directory at that spelling is not a manifest, so the walk continues.
1913
+ function readPackageType(installed: string, target: string): unknown {
1914
+ let directory = dirname(join(installed, target))
1915
+ while (true) {
1916
+ const path = join(directory, 'package.json')
1917
+ if (matchesFile(path)) {
1918
+ try {
1919
+ const manifest = readJson(path)
1920
+ return isRecord(manifest) ? manifest.type : undefined
1921
+ } catch {
1922
+ return undefined
1923
+ }
1924
+ }
1925
+ if (directory === installed) return undefined
1926
+ const parent = dirname(directory)
1927
+ if (parent === directory) return undefined
1928
+ directory = parent
1929
+ }
1930
+ }
1931
+
1932
+ // Whether the declaration selected by a typed CommonJS consumer admits that entry
1933
+ // to its compile probe. A \`.d.cts\` declaration admits and a \`.d.mts\`
1934
+ // declaration refuses. A \`.d.ts\` declaration takes its own nearest package scope.
1935
+ // Runtime target format remains the runtime drive's separate question.
1936
+ function resolvesCommonJS(entry: unknown, installed: string): boolean {
1937
+ const declaration = resolveDeclaration(entry, DECLARATION_CONDITIONS.commonjs, installed)
1938
+ if (declaration === undefined) return false
1939
+ if (declaration.endsWith('.d.cts')) return true
1940
+ if (declaration.endsWith('.d.mts')) return false
1941
+ return declaration.endsWith('.d.ts') && readPackageType(installed, declaration) !== 'module'
1942
+ }
1943
+
1944
+ // Every target an entry names under any condition. A fallback list omits members
1945
+ // Node rejects during package-target validation, because no reader can take them.
1946
+ function collectTargets(entry: unknown): readonly string[] {
1947
+ if (typeof entry === 'string') return [entry]
1948
+ if (isList(entry)) return entry.flatMap(collectTargets).filter(isPackageTarget)
1949
+ if (!isRecord(entry)) return []
1950
+ return Object.values(entry).flatMap((nested) => collectTargets(nested))
1951
+ }
1952
+
1953
+ // Whether a target is a file a runtime loads for its names, which is what a
1954
+ // declaration is owed for. The extension on the target's own file name decides it,
1955
+ // and a name carrying no extension is code: \`require\` reads such a file through its
1956
+ // JavaScript handler, so an extensionless target loads and publishes names. Node
1957
+ // loads \`.node\` through its native-addon handler. Every other extension is an asset
1958
+ // a consumer reads rather than imports — a stylesheet, a WebAssembly binary, the
1959
+ // \`"./package.json"\` manifest pointer, and a declaration alike.
1960
+ // The cost is an extensionless file published for a reader, such as a \`LICENSE\`:
1961
+ // that target reports undeclared until it is given an extension or a declaration.
1962
+ function isModule(target: string): boolean {
1963
+ const name = target.slice(target.lastIndexOf('/') + 1)
1964
+ const dot = name.lastIndexOf('.')
1965
+ if (name.endsWith(ADDON_EXTENSION)) return true
1966
+ return dot === -1 || MODULE_EXTENSIONS.includes(name.slice(dot))
1967
+ }
1968
+
1969
+ // Whether a resolved target is a declaration rather than the JavaScript a
1970
+ // \`default\` branch answers with when the entry declares no \`types\` condition.
1971
+ function isDeclaration(target: string): boolean {
1972
+ return DECLARATION_EXTENSIONS.some((extension) => target.endsWith(extension))
1973
+ }
1974
+
1975
+ // The declarations the Node module, Node CommonJS, and browser drives compare
1976
+ // against. Each field uses the conditions of the TypeScript consumer paired with
1977
+ // that runtime. A JavaScript target resolves through TypeScript's adjacent
1978
+ // declaration substitution rather than standing in for the declaration itself.
1979
+ function readDeclaration(entry: unknown, installed: string): Entry['declaration'] {
1980
+ return {
1981
+ module: resolveDeclaration(entry, DECLARATION_CONDITIONS.module, installed),
1982
+ commonjs: resolveDeclaration(entry, DECLARATION_CONDITIONS.commonjs, installed),
1983
+ browser: resolveDeclaration(entry, DECLARATION_CONDITIONS.browser, installed),
1984
+ }
1985
+ }
1986
+
1987
+ // The entries one compile driver can resolve under its own conditions.
1988
+ function selectEntries(entries: readonly Entry[], conditions: readonly string[]): readonly Entry[] {
1989
+ return entries.filter(
1990
+ (entry) =>
1991
+ resolveTarget(entry.mapping, conditions) !== undefined &&
1992
+ (!conditions.includes('require') || entry.commonjs),
1993
+ )
1994
+ }
1995
+
1996
+ // Require-loadable entries that declare CommonJS support but a typed CommonJS
1997
+ // consumer cannot compile against. A default branch resolving under the require
1998
+ // condition set makes no CommonJS claim.
1999
+ function selectUntypable(entries: readonly Entry[]): readonly Entry[] {
2000
+ return entries.filter(
2001
+ (entry) =>
2002
+ entry.required &&
2003
+ isRecord(entry.mapping) &&
2004
+ Object.hasOwn(entry.mapping, 'require') &&
2005
+ !entry.commonjs,
2006
+ )
2007
+ }
2008
+
2009
+ // The value exports a declaration publishes, read through the compiler's checker
2010
+ // over the module symbol rather than off the declaration text. An alias resolves to
2011
+ // what it names, so a re-export counts as the thing it re-exports, and a type-only
2012
+ // symbol is dropped because no runtime publishes one.
2013
+ function readDeclaredExports(declaration: string): readonly string[] {
2014
+ const program = ts.createProgram([declaration], {
2015
+ module: ts.ModuleKind.ESNext,
2016
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
2017
+ noEmit: true,
2018
+ skipLibCheck: true,
2019
+ target: ts.ScriptTarget.ESNext,
2020
+ })
2021
+ const source = program.getSourceFile(declaration)
2022
+ if (source === undefined) throw new Error(\`The declaration \${declaration} was not read\`)
2023
+ const checker = program.getTypeChecker()
2024
+ const symbol = checker.getSymbolAtLocation(source)
2025
+ if (symbol === undefined) throw new Error(\`\${declaration} declares no module symbol\`)
2026
+ const values: string[] = []
2027
+ for (const exported of checker.getExportsOfModule(symbol)) {
2028
+ const direct = (exported.flags & ts.SymbolFlags.Alias) === 0
2029
+ const resolved = direct ? exported : checker.getAliasedSymbol(exported)
2030
+ if ((resolved.flags & ts.SymbolFlags.Value) !== 0) values.push(exported.getName())
2031
+ }
2032
+ return [...values].sort()
2033
+ }
2034
+
2035
+ // The diagnostics a consumer compiling against the installed declarations reports,
2036
+ // flattened to their messages so a failure names what the consumer could not do.
2037
+ function compileConsumer(
2038
+ entry: string,
2039
+ resolution: ts.ModuleResolutionKind,
2040
+ module: ts.ModuleKind,
2041
+ ): readonly string[] {
2042
+ const program = ts.createProgram([entry], {
2043
+ module,
2044
+ moduleResolution: resolution,
2045
+ noEmit: true,
2046
+ skipLibCheck: true,
2047
+ strict: true,
2048
+ target: ts.ScriptTarget.ESNext,
2049
+ })
2050
+ return ts
2051
+ .getPreEmitDiagnostics(program)
2052
+ .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '))
2053
+ }
2054
+
2055
+ // One consumer module importing every installed entry, written where its own
2056
+ // resolution finds the installed package.
2057
+ function writeConsumerProbe(stage: Stage, path: string, specifiers: readonly string[]): string {
2058
+ const names: string[] = []
2059
+ const bindings: string[] = []
2060
+ for (const [index, specifier] of specifiers.entries()) {
2061
+ const binding = \`entry\${String(index)}\`
2062
+ names.push(binding)
2063
+ bindings.push(\`import * as \${binding} from \${JSON.stringify(specifier)}\`)
2064
+ }
2065
+ const target = join(stage.consumer, path)
2066
+ writeFile(target, \`\${bindings.join('\\n')}\\nexport const surface = [\${names.join(', ')}]\\n\`)
2067
+ return target
2068
+ }
2069
+
2070
+ // The runtime key set a real process reads off one installed entry under one
2071
+ // condition. The driver is a file rather than an \`--eval\` string, so the specifier
2072
+ // travels as an argument and nothing needs escaping.
2073
+ function driveRuntime(stage: Stage, specifier: string, driver: string): readonly string[] {
2074
+ const result = runNode([join(stage.consumer, driver), specifier], stage.consumer)
2075
+ if (result.status !== 0) {
2076
+ throw new Error(\`Loading \${specifier} from the consumer failed: \${readOutput(result)}\`)
2077
+ }
2078
+ const published: unknown = JSON.parse(result.stdout)
2079
+ if (!isNames(published)) throw new Error(\`The driver printed no name list for \${specifier}\`)
2080
+ return published
2081
+ }
2082
+ {{helpers}}
2083
+ // Pack this workspace, install the archive into an isolated consumer, and read the
2084
+ // published surface back off the installed tree. Every later claim reads this
2085
+ // result, so a failure here is raised where it happens rather than once per entry.
2086
+ function buildStage(): Stage {
2087
+ const packed = join(SCRATCH, 'packed')
2088
+ const consumer = join(SCRATCH, 'consumer')
2089
+ mkdirSync(packed, { recursive: true })
2090
+ const pack = runNpm(['pack', '--ignore-scripts', '--pack-destination', packed], ROOT)
2091
+ if (pack.status !== 0) throw new Error(\`npm pack refused this workspace: \${readOutput(pack)}\`)
2092
+ const archives = readdirSync(packed).filter((name) => name.endsWith('.tgz'))
2093
+ const archive = archives[0]
2094
+ if (archives.length !== 1 || archive === undefined) {
2095
+ throw new Error(\`npm pack wrote no single archive: \${archives.join(', ')}\`)
2096
+ }
2097
+ writeFile(join(consumer, 'package.json'), CONSUMER_MANIFEST)
2098
+ writeFile(join(consumer, ESM_DRIVER), ESM_DRIVER_SOURCE)
2099
+ writeFile(join(consumer, CJS_DRIVER), CJS_DRIVER_SOURCE)
2100
+ const install = runNpm(
2101
+ ['install', '--ignore-scripts', '--no-audit', '--no-fund', join(packed, archive)],
2102
+ consumer,
2103
+ )
2104
+ if (install.status !== 0) {
2105
+ throw new Error(\`Installing the packed archive failed: \${readOutput(install)}\`)
2106
+ }
2107
+ const name = readManifestName(join(ROOT, 'package.json'))
2108
+ const installed = join(consumer, 'node_modules', ...name.split('/'))
2109
+ const manifest = readJson(join(installed, 'package.json'))
2110
+ if (!isRecord(manifest) || !isRecord(manifest.exports)) {
2111
+ throw new Error('The installed manifest publishes no exports map')
2112
+ }
2113
+ const entries: Entry[] = []
2114
+ const targets: string[] = []
2115
+ const subpaths: string[] = []
2116
+ const undeclared: string[] = []
2117
+ const excluded: string[] = []
2118
+ for (const [subpath, entry] of Object.entries(manifest.exports)) {
2119
+ const files = collectTargets(entry)
2120
+ targets.push(...files)
2121
+ subpaths.push(subpath)
2122
+ const declaration = readDeclaration(entry, installed)
2123
+ // A subpath resolving no declaration is partitioned rather than dropped. It is a
2124
+ // defect when a runtime loads one of its targets for names, because a consumer
2125
+ // importing it compiles against nothing under \`node16\`. It is an excluded
2126
+ // publication otherwise: the \`"./package.json"\` manifest pointer and a stylesheet
2127
+ // are published for a reader rather than an importer.
2128
+ if (
2129
+ declaration.module === undefined &&
2130
+ declaration.commonjs === undefined &&
2131
+ declaration.browser === undefined
2132
+ ) {
2133
+ if (files.some(isModule)) undeclared.push(subpath)
2134
+ else excluded.push(subpath)
2135
+ continue
2136
+ }
2137
+ const imported = resolveTarget(entry, RUNTIME_CONDITIONS.module)
2138
+ const commonjs = resolvesCommonJS(entry, installed)
2139
+ const required = resolveTarget(entry, RUNTIME_CONDITIONS.commonjs) !== undefined
2140
+ const module = resolveTarget(entry, RUNTIME_CONDITIONS.browser)
2141
+ entries.push({
2142
+ subpath,
2143
+ specifier: subpath === '.' ? name : \`\${name}\${subpath.slice(1)}\`,
2144
+ mapping: entry,
2145
+ declaration: {
2146
+ module: declaration.module === undefined ? undefined : join(installed, declaration.module),
2147
+ commonjs:
2148
+ declaration.commonjs === undefined ? undefined : join(installed, declaration.commonjs),
2149
+ browser:
2150
+ declaration.browser === undefined ? undefined : join(installed, declaration.browser),
2151
+ },
2152
+ browser: module !== undefined && module.startsWith(BROWSER_OUTPUT),
2153
+ module: imported !== undefined,
2154
+ commonjs,
2155
+ required,
2156
+ })
2157
+ }
2158
+ return { consumer, installed, archives, entries, subpaths, undeclared, excluded, targets }
2159
+ }
2160
+
2161
+ const SCRATCH = mkdtempSync(join(tmpdir(), 'distribution-'))
2162
+ const CACHE = join(SCRATCH, 'cache')
2163
+ mkdirSync(CACHE, { recursive: true })
2164
+ // The scratch tree holds the npm cache, the packed archive, and the installed
2165
+ // consumer, so its removal is registered before the first thing that can throw.
2166
+ afterAll(() => {
2167
+ rmSync(SCRATCH, { force: true, recursive: true })
2168
+ })
2169
+
2170
+ // Installing the packed archive resolves its own runtime dependencies, so an
2171
+ // unreachable registry leaves nothing to measure. Under release that is the gate
2172
+ // failing; anywhere else the suite skips and names the mechanism it wanted.
2173
+ //
2174
+ // A module that throws while loading never reaches the \`afterAll\` it registered,
2175
+ // so every throw here removes the scratch tree on its way out.
2176
+ function openStage(): Stage | undefined {
2177
+ try {
2178
+ if (runNpm(PING, ROOT).status !== 0) {
2179
+ if (!RELEASE) return undefined
2180
+ throw new Error(
2181
+ 'The release gate requires a reachable npm registry, and npm ping did not answer',
2182
+ )
2183
+ }
2184
+ return buildStage()
2185
+ } catch (error) {
2186
+ rmSync(SCRATCH, { force: true, recursive: true })
2187
+ throw error
2188
+ }
2189
+ }
2190
+
2191
+ const STAGE = openStage()
2192
+ const STAGED = STAGE !== undefined
2193
+
2194
+ // The staged consumer, or a skip naming what the run could not reach. \`it.skipIf\`
2195
+ // carries no reason, so the gate sits here where the test context can state one.
2196
+ function requireStage(context: TestContext): Stage {
2197
+ if (!STAGED) {
2198
+ return context.skip('\`npm ping\` did not answer, so nothing was packed or installed')
2199
+ }
2200
+ return STAGE
2201
+ }
2202
+
2203
+ describe('installed package consumer', () => {
2204
+ it('packs one archive and installs it in isolation [requires the registry]', (context) => {
2205
+ const stage = requireStage(context)
2206
+ expect(stage.archives).toHaveLength(1)
2207
+ expect(existsSync(join(stage.installed, 'package.json'))).toBe(true)
2208
+ expect(stage.entries.length).toBeGreaterThan(0)
2209
+ })
2210
+
2211
+ it('ships every relative target its exports map names [requires the registry]', (context) => {
2212
+ const stage = requireStage(context)
2213
+ const relative = stage.targets.filter((target) => target.startsWith('./'))
2214
+ expect(relative).not.toStrictEqual([])
2215
+ expect(relative.filter((target) => !existsSync(join(stage.installed, target)))).toStrictEqual(
2216
+ [],
2217
+ )
2218
+ })
2219
+
2220
+ // Every published subpath is driven, excluded by name, or reported here. A dropped
2221
+ // one leaves no trace: no runtime test, no declaration comparison, and no place in
2222
+ // the resolution compile, so the run reports success for a subpath it never
2223
+ // measured.
2224
+ it('declares types for every module it publishes [requires the registry]', (context) => {
2225
+ const stage = requireStage(context)
2226
+ const partitioned = [
2227
+ ...stage.entries.map((entry) => entry.subpath),
2228
+ ...stage.undeclared,
2229
+ ...stage.excluded,
2230
+ ]
2231
+ expect(stage.undeclared).toStrictEqual([])
2232
+ expect(partitioned.sort()).toStrictEqual([...stage.subpaths].sort())
2233
+ // A driven subpath answers a runtime condition. One resolving a declaration and
2234
+ // no Node or browser target compiles for a consumer and throws when that consumer
2235
+ // loads it. Each later drive retires itself for that entry, so this assertion names
2236
+ // the subpath rather than counting it as driven.
2237
+ const unreachable = stage.entries.filter(
2238
+ (entry) => !entry.module && !entry.required && !entry.browser,
2239
+ )
2240
+ expect(unreachable.map((entry) => entry.subpath)).toStrictEqual([])
2241
+ const untypable = selectUntypable(stage.entries)
2242
+ expect(untypable.map((entry) => entry.subpath)).toStrictEqual([])
2243
+ })
2244
+
2245
+ it('refuses a subpath its exports map does not name [requires the registry]', (context) => {
2246
+ const stage = requireStage(context)
2247
+ const name = readManifestName(join(stage.installed, 'package.json'))
2248
+ const driver = join(stage.consumer, ESM_DRIVER)
2249
+ const result = runNode([driver, \`\${name}\${ABSENT_SUBPATH}\`], stage.consumer)
2250
+ expect(result.status).not.toBe(0)
2251
+ expect(readOutput(result)).toContain('ERR_PACKAGE_PATH_NOT_EXPORTED')
2252
+ })
2253
+
2254
+ // The absent subpath is the firing control: a resolution that reports nothing
2255
+ // for every published entry has not been shown to resolve anything at all. Each
2256
+ // module format carries its own control, because a format that resolves nothing
2257
+ // is silent for the same reason a resolution that resolves nothing is.
2258
+ it('compiles a consumer under every module resolution [requires the registry]', (context) => {
2259
+ const stage = requireStage(context)
2260
+ const name = readManifestName(join(stage.installed, 'package.json'))
2261
+ const reported: string[] = []
2262
+ const silent: string[] = []
2263
+ for (const driver of RESOLUTIONS) {
2264
+ for (const [extension, format] of FORMATS) {
2265
+ const written = selectEntries(stage.entries, driver.conditions[format])
2266
+ if (written.length === 0) continue
2267
+ const specifiers = written.map((entry) => entry.specifier)
2268
+ const probe = writeConsumerProbe(stage, \`probe.\${driver.label}.\${extension}\`, specifiers)
2269
+ for (const message of compileConsumer(probe, driver.resolution, driver.module)) {
2270
+ reported.push(\`\${driver.label}.\${extension}: \${message}\`)
2271
+ }
2272
+ const absent = [\`\${name}\${ABSENT_SUBPATH}\`]
2273
+ const control = writeConsumerProbe(stage, \`control.\${driver.label}.\${extension}\`, absent)
2274
+ if (compileConsumer(control, driver.resolution, driver.module).length === 0) {
2275
+ silent.push(\`\${driver.label}.\${extension}\`)
2276
+ }
2277
+ }
2278
+ }
2279
+ expect(reported).toStrictEqual([])
2280
+ expect(silent).toStrictEqual([])
2281
+ })
2282
+ {{guard}}})
2283
+
2284
+ for (const entry of STAGE?.entries ?? []) {
2285
+ describe(\`installed entry \${entry.subpath}\`, () => {
2286
+ it.runIf(entry.module && !entry.browser)(
2287
+ 'publishes what it declares to a Node import, and no more',
2288
+ (context) => {
2289
+ const declaration = entry.declaration.module
2290
+ if (declaration === undefined) {
2291
+ throw new Error(\`\${entry.subpath} publishes no import declaration\`)
2292
+ }
2293
+ const published = driveRuntime(requireStage(context), entry.specifier, ESM_DRIVER)
2294
+ expect(published).toStrictEqual(readDeclaredExports(declaration))
2295
+ },
2296
+ )
2297
+
2298
+ it.runIf(!entry.browser && entry.required)(
2299
+ 'publishes what it declares to a Node require, and no more',
2300
+ (context) => {
2301
+ const declaration = entry.declaration.commonjs
2302
+ if (declaration === undefined) {
2303
+ throw new Error(\`\${entry.subpath} publishes no require declaration\`)
2304
+ }
2305
+ const published = driveRuntime(requireStage(context), entry.specifier, CJS_DRIVER)
2306
+ expect(published).toStrictEqual(readDeclaredExports(declaration))
2307
+ },
2308
+ )
2309
+ {{drive}} })
2310
+ }
2311
+ `,
2312
+ transport: `import { createServer } from 'node:http'
2313
+ `,
2314
+ types: `import type { PlaywrightProviderOptions } from '@vitest/browser-playwright'
2315
+ import type { Browser } from 'playwright'
2316
+ `,
2317
+ launcher: `import { chromium } from 'playwright'
2318
+ import { build } from 'vite'
2319
+ import { resolveBrowser, resolvePinnedBrowser } from '../configs/browsers.js'
2320
+ `,
2321
+ helpers: `
2322
+ const BROWSER_PAGE = \`<!doctype html>
2323
+ <html lang="en">
2324
+ <head>
2325
+ <meta charset="UTF-8" />
2326
+ <title>Distribution</title>
2327
+ </head>
2328
+ <body>
2329
+ <script type="module" src="./main.js"><\/script>
2330
+ </body>
2331
+ </html>
2332
+ \`
2333
+
2334
+ function readContentType(path: string): string {
2335
+ if (path.endsWith('.html')) return 'text/html'
2336
+ if (path.endsWith('.js')) return 'text/javascript'
2337
+ if (path.endsWith('.css')) return 'text/css'
2338
+ if (path.endsWith('.json') || path.endsWith('.map')) return 'application/json'
2339
+ return 'application/octet-stream'
2340
+ }
2341
+
2342
+ // \`resolveBrowser\` answers with provider options and never reports absence: its
2343
+ // last resort is a channel nothing verified. So the launch is attempted and its
2344
+ // rejection classified, rather than probed for and ruled on.
2345
+ function describeBrowser(options: PlaywrightProviderOptions): string {
2346
+ const endpoint = options.connectOptions?.wsEndpoint
2347
+ if (endpoint !== undefined) return \`the browser server at \${endpoint}\`
2348
+ const executable = options.launchOptions?.executablePath
2349
+ if (executable !== undefined) return \`the executable at \${executable}\`
2350
+ const channel = options.launchOptions?.channel
2351
+ if (channel !== undefined) return \`the \${channel} channel\`
2352
+ return 'the Chromium Playwright installed for itself'
2353
+ }
2354
+
2355
+ async function launchBrowser(options: PlaywrightProviderOptions): Promise<Browser> {
2356
+ const endpoint = options.connectOptions?.wsEndpoint
2357
+ if (endpoint !== undefined) return chromium.connect(endpoint)
2358
+ return chromium.launch({ ...options.launchOptions, headless: true })
2359
+ }
2360
+
2361
+ // A consumer of one installed browser entry, bundled by the Vite toolchain this
2362
+ // workspace already declares. Nothing is stubbed: the bundle resolves the installed
2363
+ // package and its whole transitive graph as an application consuming it would.
2364
+ async function bundleEntry(stage: Stage, entry: Entry): Promise<string> {
2365
+ const page = join(stage.consumer, 'pages', entry.subpath.replaceAll(/[^\\w]+/gu, '-'))
2366
+ const specifier = JSON.stringify(entry.specifier)
2367
+ writeFile(join(page, 'index.html'), BROWSER_PAGE)
2368
+ writeFile(
2369
+ join(page, 'main.js'),
2370
+ \`import * as entry from \${specifier}\\nglobalThis.subject = Object.keys(entry).sort()\\n\`,
2371
+ )
2372
+ await build({
2373
+ base: './',
2374
+ build: { emptyOutDir: true, outDir: 'bundle' },
2375
+ configFile: false,
2376
+ logLevel: 'error',
2377
+ root: page,
2378
+ })
2379
+ return join(page, 'bundle')
2380
+ }
2381
+
2382
+ // The key set the bundled module publishes in a real browser, read off the page
2383
+ // once it has loaded over a loopback server. A module that never evaluated
2384
+ // publishes nothing, and a page error is raised rather than compared away.
2385
+ async function readBrowserExports(browser: Browser, bundle: string): Promise<readonly string[]> {
2386
+ const server = createServer((request, response) => {
2387
+ const asked = request.url === undefined || request.url === '/' ? '/index.html' : request.url
2388
+ const path = join(bundle, decodeURIComponent(asked))
2389
+ if (!path.startsWith(bundle) || !existsSync(path)) {
2390
+ response.writeHead(404)
2391
+ response.end()
2392
+ return
2393
+ }
2394
+ response.writeHead(200, { 'content-type': readContentType(path) })
2395
+ response.end(readFileSync(path))
2396
+ })
2397
+ try {
2398
+ await new Promise<void>((settle) => {
2399
+ server.listen(0, '127.0.0.1', settle)
2400
+ })
2401
+ const address = server.address()
2402
+ if (address === null || typeof address === 'string') {
2403
+ throw new Error('The bundle server bound no port')
2404
+ }
2405
+ const page = await browser.newPage()
2406
+ const failures: string[] = []
2407
+ page.on('pageerror', (error) => failures.push(String(error)))
2408
+ await page.goto(\`http://127.0.0.1:\${String(address.port)}/\`, { waitUntil: 'load' })
2409
+ const published: unknown = await page.evaluate('globalThis.subject')
2410
+ if (failures.length > 0) throw new Error(\`The bundle raised \${failures.join(' | ')}\`)
2411
+ if (!isNames(published)) throw new Error('The bundled module published no name list')
2412
+ return published
2413
+ } finally {
2414
+ server.close()
2415
+ }
2416
+ }
2417
+ `,
2418
+ drive: `
2419
+ it.runIf(entry.browser)(
2420
+ 'publishes what it declares to a real browser, and no more [requires a browser]',
2421
+ async (context) => {
2422
+ const stage = requireStage(context)
2423
+ const declaration = entry.declaration.browser
2424
+ if (declaration === undefined) {
2425
+ throw new Error(\`\${entry.subpath} publishes no browser declaration\`)
2426
+ }
2427
+ const options = resolveBrowser(resolvePinnedBrowser(), process.platform, process.env)
2428
+ const browser = await launchBrowser(options).catch((error: unknown) => {
2429
+ const cause = \`\${describeBrowser(options)} was rejected: \${String(error)}\`
2430
+ if (RELEASE) throw new Error(\`The release gate requires a browser, and \${cause}\`)
2431
+ return context.skip(\`No browser launched. \${cause}\`)
2432
+ })
2433
+ try {
2434
+ const bundle = await bundleEntry(stage, entry)
2435
+ expect(await readBrowserExports(browser, bundle)).toStrictEqual(
2436
+ readDeclaredExports(declaration),
2437
+ )
2438
+ } finally {
2439
+ await browser.close()
2440
+ }
2441
+ },
2442
+ )
2443
+ `,
2444
+ guard: `
2445
+ // This proof drives a Node import and a Node require and carries no browser
2446
+ // branch: the workspace published no browser face when it was written, and the
2447
+ // browser drive measures the packed artifact, so only a published face is owed
2448
+ // one. A private browser application does not select this branch. It declares the
2449
+ // browser launcher and its Vitest browser provider and gets the generated browser
2450
+ // configuration module beside it, but installed browser tooling does not stand for
2451
+ // a published browser face. \`vite\` selects nothing either, though the branch
2452
+ // imports it: scaffold puts \`vite\` in every workspace's base development
2453
+ // dependencies, whatever that workspace publishes. The later Node
2454
+ // \`it.runIf\` predicates retire each matching Node drive for a face published
2455
+ // later, which leaves nothing measuring it. So it reddens here and names the
2456
+ // subpath a browser branch is owed for. A workspace that gains one deletes this
2457
+ // file and runs the \`repair\` verb, which writes the variant carrying that branch.
2458
+ it('publishes no browser face this proof cannot drive [requires the registry]', (context) => {
2459
+ const stage = requireStage(context)
2460
+ const faces = stage.entries.filter((entry) => entry.browser)
2461
+ expect(faces.map((entry) => entry.subpath)).toStrictEqual([])
2462
+ })
2463
+ `
2464
+ }),
1590
2465
  integration: `{{imports}}import { describe, expect, it } from 'vitest'
1591
2466
 
1592
2467
  describe('workspace integration', () => {
@@ -1866,6 +2741,37 @@ var isDependency = recordOf({
1866
2741
  optional: isBoolean
1867
2742
  }, ["optional"]);
1868
2743
  /**
2744
+ * Narrow a value to a {@link ManifestScript}.
2745
+ *
2746
+ * @remarks
2747
+ * Structural and bounded, exactly as {@link isDependency} is: a script name
2748
+ * and a script command are free text a manifest may carry, and which values a
2749
+ * region writer is willing to overwrite is the caller's decision rather than
2750
+ * this guard's.
2751
+ *
2752
+ * @example
2753
+ * ```ts
2754
+ * import { isManifestScript } from '@orkestrel/scaffold'
2755
+ *
2756
+ * isManifestScript({ name: 'test', command: 'vitest run', accepted: [] }) // true
2757
+ * isManifestScript({ name: 'test', command: 'vitest run' }) // false
2758
+ * ```
2759
+ */
2760
+ var isManifestScript = recordOf({
2761
+ name: stringOf({
2762
+ min: 1,
2763
+ max: MAX_SCRIPT_LENGTH
2764
+ }),
2765
+ command: stringOf({
2766
+ min: 1,
2767
+ max: MAX_SCRIPT_LENGTH
2768
+ }),
2769
+ accepted: andOf(isCollection, arrayOf(stringOf({
2770
+ min: 1,
2771
+ max: MAX_SCRIPT_LENGTH
2772
+ })))
2773
+ });
2774
+ /**
1869
2775
  * Narrow a value to an {@link Override}.
1870
2776
  *
1871
2777
  * @remarks
@@ -1917,7 +2823,6 @@ var isBlueprint = recordOf({
1917
2823
  bin: isBoolean,
1918
2824
  setup: isBoolean,
1919
2825
  guides: isBoolean,
1920
- distribution: isBoolean,
1921
2826
  integration: isBoolean,
1922
2827
  conformance: isBoolean,
1923
2828
  service: isBoolean,
@@ -1971,14 +2876,16 @@ var isArtifact = unionOf(recordOf({
1971
2876
  * @remarks
1972
2877
  * A plan reaches the writer, and the writer has no question channel, so this
1973
2878
  * carries the whole law of the value: every artifact path, every claimed byte,
1974
- * and the blueprint it was compiled from.
2879
+ * and the blueprint it was compiled from. An artifact at {@link MANIFEST_PATH}
2880
+ * must carry `birth` ownership. A plan claiming `content` or `presence` there
2881
+ * is refused because the compiler emits the manifest only as birth-owned.
1975
2882
  */
1976
- var isPlan = recordOf({
2883
+ var isPlan = andOf(recordOf({
1977
2884
  blueprint: isBlueprint,
1978
2885
  groups: isGroups,
1979
2886
  artifacts: andOf(isCollection, arrayOf(isArtifact)),
1980
2887
  hash: isHex
1981
- }, ["hash"]);
2888
+ }, ["hash"]), (plan) => plan.artifacts.every((artifact) => artifact.path !== "package.json" || artifact.ownership === "birth"));
1982
2889
  /**
1983
2890
  * Narrow a value to a {@link Question}.
1984
2891
  *
@@ -2061,7 +2968,7 @@ var isMirror = unionOf(recordOf({
2061
2968
  }, ["observed"]), recordOf({
2062
2969
  name: isDependencyName,
2063
2970
  path: isPath,
2064
- lookup: literalOf("missing", "failed"),
2971
+ lookup: literalOf("missing", "unmatched", "failed"),
2065
2972
  note: isString,
2066
2973
  observed: isHex
2067
2974
  }, ["observed"]));
@@ -2079,7 +2986,7 @@ var isCatalogEntry = unionOf(recordOf({
2079
2986
  dependencies: andOf(isCollection, arrayOf(isDependency))
2080
2987
  }), recordOf({
2081
2988
  name: isDependencyName,
2082
- lookup: literalOf("missing", "failed"),
2989
+ lookup: literalOf("missing", "unmatched", "failed"),
2083
2990
  note: isString
2084
2991
  }));
2085
2992
  /**
@@ -2410,6 +3317,29 @@ function matchesOrchestrationPath(path) {
2410
3317
  return ORCHESTRATION_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
2411
3318
  }
2412
3319
  /**
3320
+ * Checks whether another surface owns the vendored bytes at a path.
3321
+ *
3322
+ * @param path - The target-relative vendored path to test.
3323
+ * @returns `true` for the catalog agent file and for a Markdown guide mirror;
3324
+ * `false` otherwise.
3325
+ *
3326
+ * @remarks
3327
+ * The materializer keeps these paths presence-owned because the catalog or
3328
+ * mirror surface writes their bytes. A live host fill therefore keeps the
3329
+ * installed floor's bytes for them and requests only the host-owned paths.
3330
+ *
3331
+ * @example
3332
+ * ```ts
3333
+ * import { isDeferredPath } from '@orkestrel/scaffold'
3334
+ *
3335
+ * isDeferredPath('guides/router.md') // true
3336
+ * isDeferredPath('AGENTS.md') // false
3337
+ * ```
3338
+ */
3339
+ function isDeferredPath(path) {
3340
+ return path === ".claude/agents/orkestrel.md" || path.startsWith("guides/") && path.endsWith(".md");
3341
+ }
3342
+ /**
2413
3343
  * Infer the {@link Group} a path belongs to.
2414
3344
  *
2415
3345
  * @param path - The target-relative path to classify.
@@ -2999,19 +3929,17 @@ function manifestToName(manifest) {
2999
3929
  return name;
3000
3930
  }
3001
3931
  /**
3002
- * Project a package manifest's text to the `@orkestrel/*` packages it declares.
3932
+ * Project a package manifest's text to the `@orkestrel/*` packages each dependency section declares.
3003
3933
  *
3004
3934
  * @param manifest - The `package.json` text.
3005
- * @returns One dependency per declared `@orkestrel` package, in section order,
3006
- * with the first declaration of a repeated name winning.
3935
+ * @returns The runtime, development, and peer declarations as separate lists.
3007
3936
  *
3008
3937
  * @remarks
3009
- * Runtime, development, and peer sections are read in that order, because a
3010
- * package the fleet publishes is upstream of this workspace wherever it is
3011
- * declared. Every other name is skipped rather than refused: a workspace's
3012
- * unrelated dependencies are not this package's to report on.
3938
+ * Every other name is skipped rather than refused: a workspace's unrelated
3939
+ * dependencies are not this package's to report on. Keeping the sections
3940
+ * separate prevents a caller from treating a peer as a writable floor.
3013
3941
  *
3014
- * Never throws, and every row it returns satisfies `isDependency` while the
3942
+ * Never throws, and every row it returns satisfies `isDependency` while each
3015
3943
  * list satisfies `isCollection`, so the result crosses the compiler's own
3016
3944
  * boundary without a second cleaning.
3017
3945
  *
@@ -3020,34 +3948,55 @@ function manifestToName(manifest) {
3020
3948
  * import { manifestToDependencies } from '@orkestrel/scaffold'
3021
3949
  *
3022
3950
  * manifestToDependencies('{"dependencies":{"@orkestrel/emitter":"^0.0.5","vite":"~8.2.0"}}')
3023
- * // [{ name: '@orkestrel/emitter', range: '^0.0.5' }]
3951
+ * // { runtime: [{ name: '@orkestrel/emitter', range: '^0.0.5' }], development: [], peer: [] }
3024
3952
  * ```
3025
3953
  */
3026
3954
  function manifestToDependencies(manifest) {
3027
- if (computeBytes(manifest) > 1048576) return [];
3955
+ if (computeBytes(manifest) > 1048576) return {
3956
+ runtime: [],
3957
+ development: [],
3958
+ peer: []
3959
+ };
3028
3960
  const parsed = parseJSON(manifest);
3029
- if (!isRecord(parsed)) return [];
3030
- const dependencies = [];
3031
- const seen = /* @__PURE__ */ new Set();
3032
- for (const section of [
3033
- "dependencies",
3034
- "devDependencies",
3035
- "peerDependencies"
3036
- ]) {
3037
- const entries = parsed[section];
3961
+ if (!isRecord(parsed)) return {
3962
+ runtime: [],
3963
+ development: [],
3964
+ peer: []
3965
+ };
3966
+ const runtime = [];
3967
+ const development = [];
3968
+ const peer = [];
3969
+ const sections = [
3970
+ {
3971
+ name: "dependencies",
3972
+ dependencies: runtime
3973
+ },
3974
+ {
3975
+ name: "devDependencies",
3976
+ dependencies: development
3977
+ },
3978
+ {
3979
+ name: "peerDependencies",
3980
+ dependencies: peer
3981
+ }
3982
+ ];
3983
+ for (const section of sections) {
3984
+ const entries = parsed[section.name];
3038
3985
  if (!isRecord(entries)) continue;
3039
3986
  for (const [name, range] of Object.entries(entries)) {
3040
- if (seen.has(name)) continue;
3041
3987
  if (!DEPENDENCY_NAME_PATTERN.test(name) || name.length > 214) continue;
3042
3988
  if (!isString(range) || range.length === 0 || range.length > 2048) continue;
3043
- seen.add(name);
3044
- dependencies.push({
3989
+ section.dependencies.push({
3045
3990
  name,
3046
3991
  range
3047
3992
  });
3048
3993
  }
3049
3994
  }
3050
- return limitEntries(dependencies, MAX_COLLECTION_ITEMS);
3995
+ return {
3996
+ runtime: limitEntries(runtime, MAX_COLLECTION_ITEMS),
3997
+ development: limitEntries(development, MAX_COLLECTION_ITEMS),
3998
+ peer: limitEntries(peer, MAX_COLLECTION_ITEMS)
3999
+ };
3051
4000
  }
3052
4001
  //#endregion
3053
4002
  //#region src/core/compilers.ts
@@ -3253,13 +4202,15 @@ function blueprintToDevDependencies(blueprint) {
3253
4202
  * proofs every workspace can pass before it has a public API, and one build per
3254
4203
  * target that actually builds.
3255
4204
  *
3256
- * A publishing workspace isolates distribution and live-service proofs from
3257
- * `test` and runs them from `prepublishOnly` instead. A private workspace has
3258
- * no publish lifecycle, so it omits distribution and runs a live-service proof
3259
- * from `test`. Integration and conformance stay in `test` because they neither
3260
- * pack nor install the workspace and drive no external service. A conformance
3261
- * run may start a server, but it starts its own and reaches it over loopback,
3262
- * so the run stays hermetic.
4205
+ * A publishing workspace isolates its distribution and live-service proofs from
4206
+ * `test` and runs them from `prepublishOnly` instead. Publishing is what selects
4207
+ * the distribution proof: what that proof measures is the packed tarball, so a
4208
+ * workspace that packs no published source has nothing for it to read. A private
4209
+ * workspace therefore omits it and runs a live-service proof from `test`.
4210
+ * Integration and conformance stay in `test` because they neither pack nor
4211
+ * install the workspace and drive no external service. A conformance run may
4212
+ * start a server, but it starts its own and reaches it over loopback, so the run
4213
+ * stays hermetic.
3263
4214
  *
3264
4215
  * The configuration paths interpolated here are the same ones `SRC_MATRIX` and
3265
4216
  * `APP_MATRIX` list as each environment's configuration files, so a rename in
@@ -3276,7 +4227,6 @@ function blueprintToDevDependencies(blueprint) {
3276
4227
  */
3277
4228
  function blueprintToScripts(blueprint) {
3278
4229
  const publishes = blueprint.src.length > 0;
3279
- const distributes = blueprint.distribution && publishes;
3280
4230
  const integrates = blueprint.integration;
3281
4231
  const compiles = publishes || blueprint.bin;
3282
4232
  const runtime = blueprint.app.filter((environment) => environment !== "core");
@@ -3334,7 +4284,7 @@ function blueprintToScripts(blueprint) {
3334
4284
  if (blueprint.conformance) scripts["test:conformance"] = `${vitest} --project conformance`;
3335
4285
  scripts["test:probe"] = "vitest run --config vite.config.ts --no-cache --reporter=verbose --project probe";
3336
4286
  scripts["test:bench"] = "vitest bench --config vite.config.ts --no-cache --project probe";
3337
- if (distributes) scripts["test:distribution"] = `${vitest} --project distribution`;
4287
+ if (publishes) scripts["test:distribution"] = `${vitest} --project distribution`;
3338
4288
  if (integrates) scripts["test:integration"] = `${vitest} --project integration`;
3339
4289
  if (blueprint.service) scripts["test:service"] = `${vitest} --project service`;
3340
4290
  scripts.build = [
@@ -3370,13 +4320,55 @@ function blueprintToScripts(blueprint) {
3370
4320
  scripts.prepack = scripts.build;
3371
4321
  scripts.prepublishOnly = [
3372
4322
  "npm run format:check && npm run lint:check && npm run check && npm run build && npm test",
3373
- ...distributes ? ["npm run test:distribution -- --mode release"] : [],
4323
+ RELEASE_PROOF_COMMAND,
3374
4324
  ...blueprint.service ? ["npm run test:service"] : []
3375
4325
  ].join(" && ");
3376
4326
  }
3377
4327
  return scripts;
3378
4328
  }
3379
4329
  /**
4330
+ * Project a blueprint into the manifest scripts a region write may replace.
4331
+ *
4332
+ * @param blueprint - The workspace specification.
4333
+ * @returns One entry per writable script, or none when the workspace publishes
4334
+ * nothing and therefore declares neither script.
4335
+ *
4336
+ * @remarks
4337
+ * Publishing is what selects these scripts, so a private workspace answers an
4338
+ * empty region and the write leaves its manifest alone.
4339
+ *
4340
+ * `accepted` carries the predecessor a target can hold before this package
4341
+ * generated the packed-package proof: the same gate chain without
4342
+ * {@link RELEASE_PROOF_COMMAND}. The value being written is always writable, so
4343
+ * it is not repeated there. Any other value is a chain the workspace author
4344
+ * customized, and {@link replaceManifestScripts} refuses the whole region
4345
+ * rather than taking it.
4346
+ *
4347
+ * @example
4348
+ * ```ts
4349
+ * import { blueprintToWritableScripts, createBlueprint } from '@orkestrel/scaffold'
4350
+ *
4351
+ * const blueprint = createBlueprint('router', { src: ['core'] })
4352
+ *
4353
+ * blueprintToWritableScripts(blueprint)[0]?.name // 'test:distribution'
4354
+ * ```
4355
+ */
4356
+ function blueprintToWritableScripts(blueprint) {
4357
+ const scripts = blueprintToScripts(blueprint);
4358
+ const distribution = scripts["test:distribution"];
4359
+ const prepublish = scripts.prepublishOnly;
4360
+ if (distribution === void 0 || prepublish === void 0) return [];
4361
+ return [{
4362
+ name: "test:distribution",
4363
+ command: distribution,
4364
+ accepted: []
4365
+ }, {
4366
+ name: "prepublishOnly",
4367
+ command: prepublish,
4368
+ accepted: [prepublish.replace(` && ${RELEASE_PROOF_COMMAND}`, "")]
4369
+ }];
4370
+ }
4371
+ /**
3380
4372
  * Compile a blueprint into its `package.json` content.
3381
4373
  *
3382
4374
  * @param blueprint - The workspace specification.
@@ -3543,7 +4535,6 @@ function blueprintToRootTsconfig(blueprint) {
3543
4535
  function blueprintToRootVite(blueprint) {
3544
4536
  const machinery = blueprintToMachinery(blueprint);
3545
4537
  const publishes = blueprint.src.length > 0;
3546
- const distributes = blueprint.distribution && publishes;
3547
4538
  const imports = [];
3548
4539
  if (machinery.browser) imports.push("import { playwright } from '@vitest/browser-playwright'");
3549
4540
  if (machinery.vue) imports.push("import vue from '@vitejs/plugin-vue'");
@@ -3656,7 +4647,7 @@ export function appShowcase(): UserConfig {
3656
4647
  showcaseBuild,
3657
4648
  showcaseFactory
3658
4649
  }));
3659
- projects.push("appBrowser()");
4650
+ projects.push("appBrowser");
3660
4651
  }
3661
4652
  if (blueprint.app.includes("server")) {
3662
4653
  factories.push(CONFIG_TEMPLATES.factories.app.server);
@@ -3682,7 +4673,7 @@ export function appShowcase(): UserConfig {
3682
4673
  factories.push(CONFIG_TEMPLATES.factories.service);
3683
4674
  projects.push("service");
3684
4675
  }
3685
- if (distributes) {
4676
+ if (publishes) {
3686
4677
  factories.push(CONFIG_TEMPLATES.factories.distribution);
3687
4678
  projects.push("distribution");
3688
4679
  }
@@ -3916,6 +4907,13 @@ function blueprintToSourceArtifacts(blueprint) {
3916
4907
  * readiness setup alone, because the root configuration names that module by
3917
4908
  * path.
3918
4909
  *
4910
+ * The distribution proof is emitted, and the same test separates it from those
4911
+ * two: its subject is the packed tarball rather than anything only the package
4912
+ * knows, so every assertion derives from the installed tree at run time and
4913
+ * nothing has to be named. It follows the published source it packs, and it is
4914
+ * the one artifact here claimed by presence: a target lacking it reports as
4915
+ * drift, and a package that replaced it with a better proof keeps that proof.
4916
+ *
3919
4917
  * @example
3920
4918
  * ```ts
3921
4919
  * import { blueprintToTestArtifacts, createBlueprint } from '@orkestrel/scaffold'
@@ -3997,6 +4995,24 @@ function blueprintToTestArtifacts(blueprint) {
3997
4995
  label: serializeTypeScriptString(`app ${environment} entry`)
3998
4996
  })
3999
4997
  });
4998
+ if (blueprint.src.length > 0) {
4999
+ const browser = blueprint.src.includes("browser");
5000
+ const distribution = ARTIFACT_TEMPLATES.tests.distribution;
5001
+ artifacts.push({
5002
+ path: DISTRIBUTION_TEST_PATH,
5003
+ group: "tests",
5004
+ ownership: "presence",
5005
+ origin: "template",
5006
+ content: fillTemplate(distribution.proof, {
5007
+ types: browser ? distribution.types : "",
5008
+ transport: browser ? distribution.transport : "",
5009
+ launcher: browser ? distribution.launcher : "",
5010
+ helpers: browser ? distribution.helpers : "",
5011
+ drive: browser ? distribution.drive : "",
5012
+ guard: browser ? "" : distribution.guard
5013
+ })
5014
+ });
5015
+ }
4000
5016
  if (blueprint.integration) {
4001
5017
  const imports = [];
4002
5018
  const entries = [];
@@ -4183,34 +5199,33 @@ function applyOverrides(artifacts, overrides) {
4183
5199
  * Replace declared dependency ranges in package manifest text.
4184
5200
  *
4185
5201
  * @param manifest - The manifest text to compile.
4186
- * @param dependencies - The declared names and replacement ranges.
4187
- * @returns The manifest with every matching dependency-section value replaced,
4188
- * or `undefined` when any name has no quoted declaration in those sections.
5202
+ * @param pins - The runtime and development names and replacement ranges.
5203
+ * @returns The manifest with every matching writable value replaced, or
5204
+ * `undefined` when a name has no quoted declaration in its named section.
4189
5205
  *
4190
5206
  * @remarks
4191
5207
  * The compiler replaces values in place instead of serializing the manifest,
4192
5208
  * so description, keywords, scripts, key order, indentation, and every byte
4193
- * outside the named ranges survive. Every occurrence in `dependencies`,
4194
- * `devDependencies`, and `peerDependencies` moves, which keeps duplicate
4195
- * declarations aligned until the manifest's own validation reports the
4196
- * duplicate. An override or resolution with the same name stays untouched.
5209
+ * outside the named ranges survive. Runtime pins apply only to `dependencies`,
5210
+ * and development pins apply only to `devDependencies`. The compiler never
5211
+ * reads or writes `peerDependencies` or `peerDependenciesMeta`. An override or
5212
+ * resolution with the same name stays untouched.
4197
5213
  *
4198
5214
  * @example
4199
5215
  * ```ts
4200
5216
  * import { replaceManifestRanges } from '@orkestrel/scaffold'
4201
5217
  *
4202
5218
  * const manifest = '{"devDependencies":{"typescript":"^6"}}\n'
4203
- * replaceManifestRanges(manifest, [{ name: 'typescript', range: '^7' }])
5219
+ * replaceManifestRanges(manifest, {
5220
+ * runtime: [],
5221
+ * development: [{ name: 'typescript', range: '^7' }],
5222
+ * })
4204
5223
  * // the manifest with the declared range replaced
4205
5224
  * ```
4206
5225
  */
4207
- function replaceManifestRanges(manifest, dependencies) {
4208
- if (dependencies.length === 0) return manifest;
4209
- const sectionNames = /* @__PURE__ */ new Set([
4210
- "dependencies",
4211
- "devDependencies",
4212
- "peerDependencies"
4213
- ]);
5226
+ function replaceManifestRanges(manifest, pins) {
5227
+ if (pins.runtime.length === 0 && pins.development.length === 0) return manifest;
5228
+ const sectionNames = /* @__PURE__ */ new Set(["dependencies", "devDependencies"]);
4214
5229
  const sections = [];
4215
5230
  let depth = 0;
4216
5231
  let cursor = 0;
@@ -4270,6 +5285,7 @@ function replaceManifestRanges(manifest, dependencies) {
4270
5285
  sectionDepth -= 1;
4271
5286
  if (sectionDepth === 0) {
4272
5287
  sections.push({
5288
+ name: key,
4273
5289
  start: value,
4274
5290
  end: sectionCursor + 1
4275
5291
  });
@@ -4283,10 +5299,13 @@ function replaceManifestRanges(manifest, dependencies) {
4283
5299
  }
4284
5300
  cursor = end;
4285
5301
  }
4286
- const replacements = new Map(dependencies.map(({ name, range }) => [name, range]));
4287
- const declared = /* @__PURE__ */ new Set();
5302
+ const runtime = /* @__PURE__ */ new Set();
5303
+ const development = /* @__PURE__ */ new Set();
4288
5304
  let compiled = manifest;
4289
5305
  for (const bounds of sections.reverse()) {
5306
+ const dependencies = bounds.name === "dependencies" ? pins.runtime : pins.development;
5307
+ const replacements = new Map(dependencies.map(({ name, range }) => [name, range]));
5308
+ const declared = bounds.name === "dependencies" ? runtime : development;
4290
5309
  let section = compiled.slice(bounds.start, bounds.end);
4291
5310
  let sectionDepth = 0;
4292
5311
  let sectionCursor = 0;
@@ -4353,13 +5372,225 @@ function replaceManifestRanges(manifest, dependencies) {
4353
5372
  }
4354
5373
  compiled = compiled.slice(0, bounds.start) + section + compiled.slice(bounds.end);
4355
5374
  }
4356
- return dependencies.every(({ name }) => declared.has(name)) ? compiled : void 0;
5375
+ const declaredRuntime = pins.runtime.every(({ name }) => runtime.has(name));
5376
+ const declaredDevelopment = pins.development.every(({ name }) => development.has(name));
5377
+ return declaredRuntime && declaredDevelopment ? compiled : void 0;
5378
+ }
5379
+ /**
5380
+ * Replace named script values in package manifest text.
5381
+ *
5382
+ * @param manifest - The manifest text to compile.
5383
+ * @param scripts - The scripts to write, each with the predecessors it accepts.
5384
+ * @returns The manifest carrying every named script, or `undefined` when a
5385
+ * named script holds a value outside what it accepts, or when the text carries
5386
+ * no readable `scripts` object to write into.
5387
+ *
5388
+ * @remarks
5389
+ * The compiler replaces values in place instead of serializing the manifest, so
5390
+ * description, keywords, dependencies, key order, indentation, and every byte
5391
+ * outside the replaced ranges survive. A named script the manifest already
5392
+ * declares is overwritten only when its value is the one being written or one
5393
+ * of its {@link ManifestScript.accepted} predecessors; anything else is a chain
5394
+ * the workspace author customized, and the whole region is refused without a
5395
+ * byte moving. A named script the manifest does not declare is appended after
5396
+ * the last declared script, copying that section's indentation. A region
5397
+ * declaring nothing takes every named script as its first entries, indented
5398
+ * from the line its own opening brace sits on.
5399
+ *
5400
+ * The refusal is whole rather than per script, so a manifest never ends up
5401
+ * holding one written script beside one refused one.
5402
+ *
5403
+ * @example
5404
+ * ```ts
5405
+ * import { replaceManifestScripts } from '@orkestrel/scaffold'
5406
+ *
5407
+ * const manifest = '{\n\t"scripts": {\n\t\t"test": "vitest run"\n\t}\n}\n'
5408
+ * replaceManifestScripts(manifest, [
5409
+ * { name: 'test', command: 'vitest run --no-cache', accepted: ['vitest run'] },
5410
+ * ])
5411
+ * // the manifest with the declared script replaced
5412
+ * ```
5413
+ */
5414
+ function replaceManifestScripts(manifest, scripts) {
5415
+ if (scripts.length === 0) return manifest;
5416
+ const parsed = parseJSON(manifest);
5417
+ if (!isRecord(parsed) || !isRecord(parsed.scripts)) return void 0;
5418
+ const declared = parsed.scripts;
5419
+ for (const script of scripts) {
5420
+ if (!Object.hasOwn(declared, script.name)) continue;
5421
+ const value = declared[script.name];
5422
+ if (!isString(value)) return void 0;
5423
+ if (value !== script.command && !script.accepted.includes(value)) return void 0;
5424
+ }
5425
+ let depth = 0;
5426
+ let cursor = 0;
5427
+ let start = -1;
5428
+ let end = -1;
5429
+ while (cursor < manifest.length && end < 0) {
5430
+ const character = manifest.charAt(cursor);
5431
+ if (character === "{") {
5432
+ depth += 1;
5433
+ cursor += 1;
5434
+ continue;
5435
+ }
5436
+ if (character === "}") {
5437
+ depth -= 1;
5438
+ cursor += 1;
5439
+ continue;
5440
+ }
5441
+ if (character !== "\"") {
5442
+ cursor += 1;
5443
+ continue;
5444
+ }
5445
+ const keyStart = cursor;
5446
+ cursor += 1;
5447
+ while (cursor < manifest.length) {
5448
+ if (manifest.charAt(cursor) === "\\") {
5449
+ cursor += 2;
5450
+ continue;
5451
+ }
5452
+ if (manifest.charAt(cursor) === "\"") break;
5453
+ cursor += 1;
5454
+ }
5455
+ if (cursor >= manifest.length) return void 0;
5456
+ const keyEnd = cursor + 1;
5457
+ cursor = keyEnd;
5458
+ if (depth !== 1) continue;
5459
+ if (parseJSON(manifest.slice(keyStart, keyEnd)) !== "scripts") continue;
5460
+ let opening = keyEnd;
5461
+ while (opening < manifest.length && /\s/u.test(manifest.charAt(opening))) opening += 1;
5462
+ if (manifest.charAt(opening) !== ":") continue;
5463
+ opening += 1;
5464
+ while (opening < manifest.length && /\s/u.test(manifest.charAt(opening))) opening += 1;
5465
+ if (manifest.charAt(opening) !== "{") continue;
5466
+ start = opening;
5467
+ let nested = 0;
5468
+ let scan = opening;
5469
+ while (scan < manifest.length) {
5470
+ const inner = manifest.charAt(scan);
5471
+ if (inner === "\"") {
5472
+ scan += 1;
5473
+ while (scan < manifest.length) {
5474
+ if (manifest.charAt(scan) === "\\") {
5475
+ scan += 2;
5476
+ continue;
5477
+ }
5478
+ if (manifest.charAt(scan) === "\"") break;
5479
+ scan += 1;
5480
+ }
5481
+ if (scan >= manifest.length) return void 0;
5482
+ } else if (inner === "{") nested += 1;
5483
+ else if (inner === "}") {
5484
+ nested -= 1;
5485
+ if (nested === 0) {
5486
+ end = scan + 1;
5487
+ break;
5488
+ }
5489
+ }
5490
+ scan += 1;
5491
+ }
5492
+ }
5493
+ if (start < 0 || end < 0) return void 0;
5494
+ const edits = [];
5495
+ const written = /* @__PURE__ */ new Set();
5496
+ let first = -1;
5497
+ let nested = 0;
5498
+ let scan = start;
5499
+ while (scan < end) {
5500
+ const character = manifest.charAt(scan);
5501
+ if (character === "{") {
5502
+ nested += 1;
5503
+ scan += 1;
5504
+ continue;
5505
+ }
5506
+ if (character === "}") {
5507
+ nested -= 1;
5508
+ scan += 1;
5509
+ continue;
5510
+ }
5511
+ if (character !== "\"") {
5512
+ scan += 1;
5513
+ continue;
5514
+ }
5515
+ const keyStart = scan;
5516
+ scan += 1;
5517
+ while (scan < end) {
5518
+ if (manifest.charAt(scan) === "\\") {
5519
+ scan += 2;
5520
+ continue;
5521
+ }
5522
+ if (manifest.charAt(scan) === "\"") break;
5523
+ scan += 1;
5524
+ }
5525
+ if (scan >= end) return void 0;
5526
+ const keyEnd = scan + 1;
5527
+ scan = keyEnd;
5528
+ if (nested !== 1) continue;
5529
+ let valueStart = keyEnd;
5530
+ while (valueStart < end && /\s/u.test(manifest.charAt(valueStart))) valueStart += 1;
5531
+ if (manifest.charAt(valueStart) !== ":") continue;
5532
+ valueStart += 1;
5533
+ while (valueStart < end && /\s/u.test(manifest.charAt(valueStart))) valueStart += 1;
5534
+ if (first < 0) first = keyStart;
5535
+ if (manifest.charAt(valueStart) !== "\"") continue;
5536
+ let valueEnd = valueStart + 1;
5537
+ while (valueEnd < end) {
5538
+ if (manifest.charAt(valueEnd) === "\\") {
5539
+ valueEnd += 2;
5540
+ continue;
5541
+ }
5542
+ if (manifest.charAt(valueEnd) === "\"") break;
5543
+ valueEnd += 1;
5544
+ }
5545
+ if (valueEnd >= end) return void 0;
5546
+ scan = valueEnd + 1;
5547
+ const key = parseJSON(manifest.slice(keyStart, keyEnd));
5548
+ const script = scripts.find((entry) => entry.name === key);
5549
+ if (script === void 0) continue;
5550
+ written.add(script.name);
5551
+ edits.push({
5552
+ start: valueStart,
5553
+ end: valueEnd + 1,
5554
+ text: JSON.stringify(script.command)
5555
+ });
5556
+ }
5557
+ const missing = scripts.filter((script) => !written.has(script.name));
5558
+ if (missing.length > 0) {
5559
+ if (first < 0) {
5560
+ const opening = manifest.lastIndexOf("\n", start);
5561
+ let column = opening + 1;
5562
+ while (column < start && /\s/u.test(manifest.charAt(column))) column += 1;
5563
+ const level = opening < 0 ? "" : manifest.slice(opening + 1, column);
5564
+ const separator = opening < 0 ? "" : `\n${level}${level}`;
5565
+ const closing = opening < 0 ? "" : `\n${level}`;
5566
+ const entries = missing.map((script) => `${separator}${JSON.stringify(script.name)}: ${JSON.stringify(script.command)}`);
5567
+ edits.push({
5568
+ start: start + 1,
5569
+ end: end - 1,
5570
+ text: `${entries.join(",")}${closing}`
5571
+ });
5572
+ } else {
5573
+ let anchor = end - 1;
5574
+ while (anchor > start && /\s/u.test(manifest.charAt(anchor - 1))) anchor -= 1;
5575
+ const newline = manifest.lastIndexOf("\n", first);
5576
+ const indent = newline < 0 ? "" : manifest.slice(newline + 1, first);
5577
+ const separator = newline < 0 || /\S/u.test(indent) ? "" : `\n${indent}`;
5578
+ edits.push({
5579
+ start: anchor,
5580
+ end: anchor,
5581
+ text: missing.map((script) => `,${separator}${JSON.stringify(script.name)}: ${JSON.stringify(script.command)}`).join("")
5582
+ });
5583
+ }
5584
+ }
5585
+ let compiled = manifest;
5586
+ for (const edit of edits.sort((left, right) => right.start - left.start)) compiled = compiled.slice(0, edit.start) + edit.text + compiled.slice(edit.end);
5587
+ return compiled;
4357
5588
  }
4358
5589
  /**
4359
5590
  * Replace dependency ranges in a plan's manifest and recompute its identity.
4360
5591
  *
4361
5592
  * @param plan - The plan carrying the manifest artifact to compile.
4362
- * @param dependencies - The declared names and replacement ranges.
5593
+ * @param pins - The runtime and development names and replacement ranges.
4363
5594
  * @returns A plan with replaced manifest ranges and a matching hash, or
4364
5595
  * `undefined` when the manifest or its identity cannot be compiled.
4365
5596
  *
@@ -4373,15 +5604,15 @@ function replaceManifestRanges(manifest, dependencies) {
4373
5604
  * ```ts
4374
5605
  * import { replacePlanRanges } from '@orkestrel/scaffold'
4375
5606
  *
4376
- * replacePlanRanges(plan, releases) // the plan carrying the resolved manifest ranges
5607
+ * replacePlanRanges(plan, pins) // the plan carrying the resolved writable ranges
4377
5608
  * ```
4378
5609
  */
4379
- function replacePlanRanges(plan, dependencies) {
5610
+ function replacePlanRanges(plan, pins) {
4380
5611
  let replaced = false;
4381
5612
  let refused = false;
4382
5613
  const artifacts = plan.artifacts.map((artifact) => {
4383
5614
  if (artifact.path !== "package.json" || artifact.origin === "host") return artifact;
4384
- const content = replaceManifestRanges(artifact.content, dependencies);
5615
+ const content = replaceManifestRanges(artifact.content, pins);
4385
5616
  if (content === void 0) {
4386
5617
  refused = true;
4387
5618
  return artifact;
@@ -4708,11 +5939,6 @@ function blueprintToQuestions(blueprint) {
4708
5939
  });
4709
5940
  vendors.add(vendor);
4710
5941
  }
4711
- if (blueprint.distribution && blueprint.src.length === 0) questions.push({
4712
- field: "distribution",
4713
- message: "distribution packs the published source, and this workspace declares none, so it emits nothing.",
4714
- blocking: false
4715
- });
4716
5942
  if (blueprint.showcase && !blueprint.app.includes("browser")) questions.push({
4717
5943
  field: "showcase",
4718
5944
  message: "showcase projects a browser app, and this workspace declares none, so it emits nothing.",
@@ -4921,7 +6147,6 @@ function createBlueprint(name, input) {
4921
6147
  bin: input?.bin ?? false,
4922
6148
  setup: input?.setup ?? false,
4923
6149
  guides: input?.guides ?? false,
4924
- distribution: input?.distribution ?? false,
4925
6150
  integration: input?.integration ?? false,
4926
6151
  conformance: input?.conformance ?? false,
4927
6152
  service: input?.service ?? false,
@@ -5234,6 +6459,6 @@ var Compiler = class {
5234
6459
  }
5235
6460
  };
5236
6461
  //#endregion
5237
- export { APP_BROWSER_DEV_DEPENDENCIES, APP_DEV_DEPENDENCIES, APP_MATRIX, APP_SERVER_DEV_DEPENDENCIES, ARTIFACT_TEMPLATES, BASE_DEV_DEPENDENCIES, BIN_CONFIGS, BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFIG_TEMPLATES, CONFORMANCE_TEST_PATH, CONTROL_CHARACTER_PATTERN, Compiler, DECLARATION_DEV_DEPENDENCIES, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, DISTRIBUTION_TEST_PATH, ENGINES_PATTERN, ENVIRONMENTS, EXECUTABLE_PATHS, EXTRA_RANGE_PATTERN, FLOOR_RANGE_PATTERN, FOREIGN_NAME_PATTERN, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HEX_PATTERN, HOST_PATHS, INTEGRATION_TEST_PATH, INVALID_PATH_CHARACTER_PATTERN, MAX_ARTIFACT_BYTES, MAX_ARTIFACT_HEX_LENGTH, MAX_AUDIT_FINDINGS, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_NAME_LENGTH, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, MINIMUM_NODE_VERSION, NAME_PATTERN, ORCHESTRATION_PATH_NAMES, ORCHESTRATION_PATH_PREFIXES, ORKESTREL_RANGE_PATTERN, PRINT_WIDTH, SERVICE_SCRIPT_PATH, SERVICE_SETUP_PATH, SERVICE_TEST_INCLUDE, SHOWCASE_CONFIG_PATH, SHOWCASE_DEV_DEPENDENCIES, SOURCE_BROWSER_DEV_DEPENDENCIES, SRC_MATRIX, ScaffoldError, TAB_WIDTH, VERSION_PATTERN, WORKSPACE_OWNED_PATHS, applyOverrides, artifactToFinding, artifactToHex, artifactsToQuestions, blueprintToConfigArtifacts, blueprintToDevDependencies, blueprintToDocumentArtifacts, blueprintToGuideArtifacts, blueprintToMachinery, blueprintToManifest, blueprintToOrchestrationArtifacts, blueprintToQuestions, blueprintToRootTsconfig, blueprintToRootVite, blueprintToScripts, blueprintToSourceArtifacts, blueprintToTestArtifacts, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, computeHash, contentToHex, createBlueprint, dependenciesToQuestions, extractRangeMajor, extractVersion, inferDrift, inferGroup, isArtifact, isAudit, isBlueprint, isCatalogEntry, isCollection, isCompilerHooks, isCompilerOptions, isContent, isDependency, isDependencyName, isEnvironment, isFinding, isGroup, isGroups, isHex, isMirror, isOverride, isPath, isPlan, isQuestion, isScaffoldError, isSnapshot, manifestToDependencies, manifestToName, matchesDriftReachability, matchesEngines, matchesOrchestrationPath, matchesPrintWidth, matchesRange, nameToGuide, nameToHostArtifacts, nameToRewrite, overridesToQuestions, parseBlueprint, parseCompilerOptions, parseGroups, parseSnapshot, pathToCondition, planToFindings, planToHash, planToSummary, replaceManifestRanges, replacePlanRanges, selectGroups, selectHostPaths, serializeTypeScriptString, srcToEntry, srcToExports, srcToRoot };
6462
+ export { APP_BROWSER_DEV_DEPENDENCIES, APP_DEV_DEPENDENCIES, APP_MATRIX, APP_SERVER_DEV_DEPENDENCIES, ARTIFACT_TEMPLATES, BASE_DEV_DEPENDENCIES, BIN_CONFIGS, BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFIG_TEMPLATES, CONFORMANCE_TEST_PATH, CONTROL_CHARACTER_PATTERN, Compiler, DECLARATION_DEV_DEPENDENCIES, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, DISTRIBUTION_TEST_PATH, ENGINES_PATTERN, ENVIRONMENTS, EXECUTABLE_PATHS, EXTRA_RANGE_PATTERN, FLOOR_RANGE_PATTERN, FOREIGN_NAME_PATTERN, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HEX_PATTERN, HOST_INVENTORY_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, INVALID_PATH_CHARACTER_PATTERN, MANIFEST_PATH, MAX_ARTIFACT_BYTES, MAX_ARTIFACT_HEX_LENGTH, MAX_AUDIT_FINDINGS, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_NAME_LENGTH, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_SCRIPT_LENGTH, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, MINIMUM_NODE_VERSION, NAME_PATTERN, ORCHESTRATION_PATH_NAMES, ORCHESTRATION_PATH_PREFIXES, ORKESTREL_RANGE_PATTERN, PRINT_WIDTH, RELEASE_PROOF_COMMAND, SERVICE_SCRIPT_PATH, SERVICE_SETUP_PATH, SERVICE_TEST_INCLUDE, SHOWCASE_CONFIG_PATH, SHOWCASE_DEV_DEPENDENCIES, SOURCE_BROWSER_DEV_DEPENDENCIES, SRC_MATRIX, ScaffoldError, TAB_WIDTH, VERSION_PATTERN, WORKSPACE_OWNED_PATHS, applyOverrides, artifactToFinding, artifactToHex, artifactsToQuestions, blueprintToConfigArtifacts, blueprintToDevDependencies, blueprintToDocumentArtifacts, blueprintToGuideArtifacts, blueprintToMachinery, blueprintToManifest, blueprintToOrchestrationArtifacts, blueprintToQuestions, blueprintToRootTsconfig, blueprintToRootVite, blueprintToScripts, blueprintToSourceArtifacts, blueprintToTestArtifacts, blueprintToWritableScripts, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, computeHash, contentToHex, createBlueprint, dependenciesToQuestions, extractRangeMajor, extractVersion, inferDrift, inferGroup, isArtifact, isAudit, isBlueprint, isCatalogEntry, isCollection, isCompilerHooks, isCompilerOptions, isContent, isDeferredPath, isDependency, isDependencyName, isEnvironment, isFinding, isGroup, isGroups, isHex, isManifestScript, isMirror, isOverride, isPath, isPlan, isQuestion, isScaffoldError, isSnapshot, manifestToDependencies, manifestToName, matchesDriftReachability, matchesEngines, matchesOrchestrationPath, matchesPrintWidth, matchesRange, nameToGuide, nameToHostArtifacts, nameToRewrite, overridesToQuestions, parseBlueprint, parseCompilerOptions, parseGroups, parseSnapshot, pathToCondition, planToFindings, planToHash, planToSummary, replaceManifestRanges, replaceManifestScripts, replacePlanRanges, selectGroups, selectHostPaths, serializeTypeScriptString, srcToEntry, srcToExports, srcToRoot };
5238
6463
 
5239
6464
  //# sourceMappingURL=index.js.map