Skip to content

fix(ttsc): make TS config loaders and the virtual layout Windows-safe#308

Merged
samchon merged 10 commits into
masterfrom
fix/windows-config-loaders
Jul 2, 2026
Merged

fix(ttsc): make TS config loaders and the virtual layout Windows-safe#308
samchon merged 10 commits into
masterfrom
fix/windows-config-loaders

Conversation

@samchon

@samchon samchon commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Overview

Completes the Windows path-hardening that #300 started, in three parts:

Follow-ups folded in from #304: the runtime hooks' isWithin normalizes separators (the synthesized manifest rootDir is slash-form C:/ while real paths are native), and the ttsx e2e fixture that mirrored the loader's rootDir: "/" synthesis now derives the volume root.

Test plan

  • New Go unit tests (one case per file, per package): the generated loader tsconfig's rootDir contains both files entries, and loaderTempBase keeps the system temp on matching volumes / moves under node_modules/.cache across volumes (the cross-volume branch runs for real on drive-letter platforms, guarded by a filepath.VolumeName probe elsewhere).
  • New ttsx e2e test locking the file-symlink virtual-entry branch; the existing outside-tsconfig-directory e2e keeps covering emit serving under a root rootDir.
  • Verified on Windows 11 (repo on D:, TEMP on C:): all six new Go tests pass, including the real cross-volume branch; full strip Go suite passes; banner/lint suites show only failures already present on unchanged master (sh-script fake launchers, ttsx subprocess resolution — environmental, tracked separately). tsc --noEmit is clean for packages/lint and packages/ttsc; changed files formatted with prettier and the repo's gofmt pipeline.
  • Not runnable locally on Windows: the TS e2e suites (DynamicExecutor limitation) — they run on Linux CI.

Deferred

Resolves #304, resolves #305, resolves #306.

🤖 Generated with Claude Code

samchon and others added 2 commits July 2, 2026 16:17
The lint fix from #300 left two byte-identical siblings hardcoding the
loader tsconfig's rootDir to "/", which is never an ancestor of
drive-letter paths (TS6059, #304). Extract the volume-root resolution
into a named loaderRootDir helper (also restoring gofmt-stable
formatting in linthost) and apply it to the banner and strip drivers.

Rooting at the volume is still not enough when the system temp dir and
the config file live on different drives: no single rootDir spans two
volumes and relative import specifiers cannot cross them (#305). Create
the ephemeral loader tree under the config's nearest
node_modules/.cache in that case — on all three Go drivers and the
lint JS factory — keeping the system temp dir whenever the volumes
already match.

Also normalize separators in the runtime hooks' isWithin so the
slash-form manifest rootDir the loader tsconfig now emits (C:/) matches
native real paths, and align the ttsx e2e fixture that mirrored the
loader's rootDir synthesis.

Resolves #304, resolves #305.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lege

linkVirtualEntry handled directories, plain files, and directory-target
symlinks in a Windows-safe way, but its final branch re-symlinked file
symlinks with a bare fs.symlinkSync — which needs
SeCreateSymbolicLinkPrivilege on Windows and crashed the run with EPERM
without admin rights or Developer Mode (#306, same class as #218).
Mirror the plain-file branch's hard-link/copy fallback, and skip a link
whose target no longer exists: it can serve no module and none of the
fallbacks can materialize it.

Resolves #306.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 07:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes Windows path/filesystem hardening for TypeScript config loaders across @ttsc/lint, @ttsc/banner, and @ttsc/strip, and makes the ttsx “virtual layout” mirroring resilient to Windows symlink privilege limitations. It aligns loader rootDir and loader temp-directory placement with Windows volume semantics and adds targeted regression tests.

Changes:

  • Make generated loader tsconfigs Windows-safe by deriving rootDir from the loader directory’s volume root (e.g. C:/) instead of hardcoding "/".
  • Avoid cross-volume failures by placing ephemeral loader trees under the config’s nearest node_modules/.cache when system temp and config are on different volumes.
  • Harden virtual layout mirroring to avoid EPERM crashes when re-symlinking file-symlink entries on Windows, and add new unit/e2e coverage around the updated behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test-ttsc/src/features/ttsx-runtime/test_ttsx_virtual_layout_mirrors_a_file_symlink_project_entry.ts Adds an e2e regression test for handling a project-root file symlink during virtual-layout mirroring.
tests/test-ttsc/src/features/ttsx-runtime/test_ttsx_serves_a_files_listed_source_outside_the_tsconfig_directory.ts Updates fixture rootDir synthesis to use the platform volume root instead of "/".
packages/ttsc/src/launcher/internal/runtimeHooks.ts Normalizes path separators in isWithin so manifest rootDir comparisons work on Windows.
packages/ttsc/src/launcher/internal/prepareExecution.ts Adds a Windows-safe fallback when re-symlinking special entries would require privileged file symlinks.
packages/strip/test/typescript_loader_tsconfig_rootdir_contains_inputs_test.go Adds a unit test asserting the generated loader tsconfig rootDir contains both files entries.
packages/strip/test/linkname_helpers_test.go Exposes additional unexported strip driver helpers to tests via go:linkname.
packages/strip/test/config_loader_temp_base_stays_on_config_volume_test.go Adds a unit test asserting loader temp base stays on the config volume when system temp differs.
packages/strip/driver/config.go Uses volume-aware loader rootDir and volume-aware temp base selection for strip TS config loading.
packages/lint/test/config/loader_temp_base_stays_on_config_volume_test.go Adds a unit test for lint loader temp base behavior across volumes.
packages/lint/test/config/loader_rootdir_contains_loader_and_config_test.go Adds a unit test for lint loader rootDir containment of loader+config inputs.
packages/lint/src/index.ts Adds JS-side loader temp base selection to avoid cross-volume path.relative/rootDir failures.
packages/lint/linthost/config.go Replaces inline rootDir resolver with a helper; adds loader temp base selection matching volume behavior.
packages/banner/test/typescript_config_loader_tsconfig_rootdir_contains_inputs_test.go Adds a unit test asserting banner loader tsconfig rootDir contains both inputs.
packages/banner/test/linkname_helpers_test.go Exposes banner loader temp base helper to tests via go:linkname.
packages/banner/test/config_loader_temp_base_stays_on_config_volume_test.go Adds a unit test asserting banner loader temp base stays on the config volume across volumes.
packages/banner/driver/banner.go Uses volume-aware loader rootDir and temp base selection for banner TS config loading.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +237 to +248
try {
fs.symlinkSync(realEntry, virtualEntry);
} catch {
if (!fs.existsSync(realEntry)) {
return;
}
try {
fs.linkSync(realEntry, virtualEntry);
} catch {
fs.copyFileSync(realEntry, virtualEntry);
}
}
Comment on lines +43 to +44
fs.symlinkSync(linkedFile, entry);
assert.equal(fs.lstatSync(entry).isSymbolicLink(), true);
Driving the whole loader chain end-to-end on a two-volume Windows
machine surfaced three gaps in the previous commit:

- A linked node_modules (junction/symlink) broke the loader: the ESM
  runtime realpaths the loader module at import time, so a relative
  config specifier computed from the link-form .cache path resolved
  against the wrong directory. The temp base is realpathed now; NTFS
  junctions defeat filepath.EvalSymlinks, so the Go drivers chase the
  link component with os.Readlink first.
- Cross-volume configs with no usable node_modules/.cache fell back to
  the system temp dir — guaranteed to fail. They now fall back to the
  config's own directory, which is always on the right volume and keeps
  the relative import a plain "./".
- isWithin now compares case-insensitively on Windows (a lowercase TEMP
  yields a c:-rooted manifest rootDir for the volume real paths spell
  C:), and is exported alongside linkVirtualEntry so the e2e suite can
  pin the Windows normalization and EPERM-fallback branches that a
  spawned run cannot reach on CI.

New coverage: the temp-base tests pin the blocked-.cache, missing
node_modules, and junction realpath branches; api e2e tests drive
linkVirtualEntry's copy fallback and dangling-entry skip (the dangling
fixture is a junction, so it exercises the real privilege-less EPERM
path on Windows) and isWithin's root/slash/case matching. Verified on
Windows 11 with the project on D: and TEMP on C:, including live
cross-drive config evaluation through real ttsx + tsgo on both the JS
factory and the Go driver paths.

Refs #304, #305, #306.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samchon

samchon commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Follow-up push (f3e6875) closing every deferred item from the PR body:

  • Linked node_modules bug found and fixed by a live cross-drive run. Driving the JS factory end-to-end (project on D:, TEMP on C:, real ttsx + tsgo) surfaced that a junction/symlinked node_modules broke the loader: the ESM runtime realpaths the loader module, so the relative config import computed from the link-form .cache path resolved against the wrong directory. The temp base is realpathed now; since NTFS junctions defeat filepath.EvalSymlinks (verified empirically), the Go drivers chase the link with os.Readlink first.
  • The residual cross-volume edge is closed: no usable node_modules/.cache now falls back to the config's own directory (always the right volume, keeps the import a plain ./), not the guaranteed-to-fail system temp dir.
  • The Windows EPERM fallback of linkVirtualEntry is now covered: linkVirtualEntry and isWithin are exported for the api e2e suite; the dangling-entry test uses a junction fixture, so it exercises the real privilege-less EPERM path (confirmed on a machine where fs.symlinkSync file links throw EPERM), and the copy-fallback test drives the same symlink→hard-link→copy chain deterministically. isWithin also folds case on Windows and has a root/slash/case matching test.
  • Temp-base tests now pin the blocked-.cache, missing-node_modules, and junction-realpath branches per package.

Verification on Windows 11 (D: repo + C: TEMP): full lint and strip Go suites pass; banner shows only the sh-script fake-launcher failures already present on unchanged master; live cross-drive TS config evaluation passes through both the JS factory and the Go driver; tsc --noEmit clean for packages/lint and packages/ttsc.

samchon and others added 2 commits July 2, 2026 17:29
Add a windows-latest matrix entry running `pnpm run test:go` as the
minimal Windows safety net — the Go unit suites cover the
platform-sensitive layers (config loaders, path handling) without the
POSIX-only e2e runner, and only need the ttsx launcher build.

Everything that kept that lane red gets fixed with it:

- scripts/test-go-utility-plugins.cjs wrote forward-slash `use` paths
  into its go.work; Go's workspace-module matching on Windows rejects
  them ("directory ... is not one of the workspace modules"), so every
  suite failed at setup. Write native separators.
- packages/ttsc's build script copied README with unix `cp`, which cmd
  has no notion of; use a node one-liner.
- The banner suites faked launchers as extensionless `#!/bin/sh`
  scripts, which Windows cannot spawn. A new writeDirectLauncher helper
  emits the sh script on POSIX and an equivalent `.cmd` on Windows,
  keeping both off the node-routed extension list.
- The tempdir-failure case pointed TMPDIR at a file; os.TempDir reads
  TMP/TEMP on Windows, so override those there.
- Two assertions were unhermetic on machines with a stray drive-root
  node_modules; they now tolerate discoveries outside the fixture.

The relative-config-path case surfaced a real regression in the new
loaderTempBase helpers: a volume-less location compared unequal to the
system temp's volume and was misrouted to the cross-volume branch,
breaking the Rel-failure contract (and creating loader dirs relative to
the cwd). All four helpers now keep the historical default for
volume-less locations, with test scenarios pinning it.

Refs #304, #305.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A locally-run paths Go test materializes its external-file fixture
inside the package directory and leaves it behind; the previous commit
swept it up unintentionally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
samchon and others added 4 commits July 2, 2026 18:01
Two problems surfaced running the paths suite on a machine whose repo
and temp dir live on different drives (the windows-latest layout):

- TestCommandRewritesAllModuleSpecifierForms materialized its external
  files-list fixture inside the package directory. With the project in
  the system temp dir that makes the tsconfig `files` list span two
  volumes, and project loading hangs until the go test timeout (#310
  tracks the sidecar behavior; tsc errors fast on the same input). The
  fixture moves to a sibling temp dir — still outside the tsconfig
  directory, same volume, and it can no longer leak into the tree when
  a test dies mid-run.
- The utility-plugin runner now prewarms the plugin/driver build before
  `go test`: the command tests `go run ./plugin` during test execution,
  so on a cold CI cache the full typescript-go compile counted against
  the 10-minute test timeout.

With these, all three utility suites pass on Windows for the first
time (banner 16s, paths 20s, strip 27s locally).

Refs #308, #310.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Residue from the two timed-out suite runs predating the fixture
relocation; the previous commit swept them up. The relocation itself
prevents any further leaks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI Windows runners hand out an 8.3 short-name TEMP (RUNNER~1), so the
helper's EvalSymlinks-normalized result never compared equal to a raw
Join of the fixture root even though both name the same directory.
Normalize the expectation the same way.

Refs #308.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
assert.ok on `entry?.isSymbolicLink()` narrows that expression, not
`entry`, so the `Dirent | undefined` from Array.find reached
linkVirtualEntry and failed typecheck. Assert existence first.

Refs #308.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samchon samchon changed the title fix: make TS config loaders and the virtual layout Windows-safe fix(ttsc): make TS config loaders and the virtual layout Windows-safe Jul 2, 2026
A trailing `>&2` glued to a payload ending in a digit would turn that
digit into a file-descriptor redirect; putting the redirect first
sidesteps both that and the trailing-space form. Also document the
cmd-metacharacter constraint on payloads.

Refs #308.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samchon samchon merged commit 02d570b into master Jul 2, 2026
35 checks passed
@samchon samchon deleted the fix/windows-config-loaders branch July 2, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment