rootless-config 1.7.4 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootless-config",
3
- "version": "1.7.4",
3
+ "version": "1.8.1",
4
4
  "description": "Store project config files outside the project root, auto-deploy them where tools expect them.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "main": "./src/index.js",
10
10
  "exports": {
11
- ".": "./src/index.js"
11
+ ".": "./src/index.js",
12
+ "./serve": "./src/serve/index.js"
12
13
  },
13
14
  "files": [
14
15
  "bin",
@@ -5,7 +5,7 @@ import { readdir, unlink } from 'node:fs/promises'
5
5
  import { createLogger } from '../../utils/logger.js'
6
6
  import { fileExists, ensureDir, atomicWrite, copyFile, readJsonFile } from '../../utils/fsUtils.js'
7
7
  import { confirm } from '../../utils/prompt.js'
8
- import { patchPackageScripts, isPatchable, NEVER_MIGRATE } from '../../core/scriptPatcher.js'
8
+ import { patchPackageScripts, isPatchable, NEVER_MIGRATE, patchServerScript } from '../../core/scriptPatcher.js'
9
9
 
10
10
  // Files that are OS/editor/VCS noise — never migrate, never touch
11
11
  const SYSTEM_FILES = new Set([
@@ -92,6 +92,17 @@ export default {
92
92
  const dest = path.join(destDir, name)
93
93
 
94
94
  if (isCleanMode) {
95
+ // For PS1 server scripts: patch $root before writing to .root/assets/
96
+ if (name.endsWith('.ps1')) {
97
+ const raw = await (await import('node:fs/promises')).readFile(src, 'utf8')
98
+ const patched = patchServerScript(raw)
99
+ if (patched) {
100
+ await atomicWrite(dest, patched)
101
+ await unlink(src)
102
+ logger.success(`Moved+patched .root/${destSubdir}/${name} (server root → project root)`)
103
+ continue
104
+ }
105
+ }
95
106
  // Binary-safe stream copy — works for .png, .js, .css, etc.
96
107
  await copyFile(src, dest)
97
108
  await unlink(src)
@@ -459,6 +459,74 @@ async function patchPackageScripts(projectRoot, configsDir, logger) {
459
459
  return changed
460
460
  }
461
461
 
462
+ /**
463
+ * Patches a PowerShell HTTP server script (.server.ps1 style) so it works
464
+ * correctly after being moved to .root/assets/.
465
+ *
466
+ * The script originally uses $PSScriptRoot as web root. After migration,
467
+ * $PSScriptRoot = .root/assets/ — so subdirectories left in the project root
468
+ * (viewer/, agreement/, etc.) are invisible to it.
469
+ *
470
+ * This patch:
471
+ * 1. Sets $root to the project root (2 dirs up from $PSScriptRoot)
472
+ * 2. Keeps $assetsRoot pointing to .root/assets/ for fallback
473
+ * 3. After path resolution, falls back to $assetsRoot when file not found in $root
474
+ * 4. Fixes the 404 page lookup to also check $assetsRoot
475
+ *
476
+ * Returns patched content, or null if the file doesn't look like a server script.
477
+ */
478
+ function patchServerScript(content) {
479
+ // Only patch scripts that use $PSScriptRoot as web root
480
+ if (!content.includes('$PSScriptRoot') || !content.includes('$root')) return null
481
+
482
+ let out = content
483
+
484
+ // 1. Redefine $root → project root (2 levels up), keep $assetsRoot for fallback
485
+ out = out.replace(
486
+ /\$root(\s*)=(\s*)\$PSScriptRoot\.TrimEnd\('\\\\?'\)\s*\+\s*'\\\\'?/,
487
+ `$assetsRoot$1=$2$PSScriptRoot.TrimEnd('\\') + '\\'
488
+ $root$1=$2(Get-Item "$PSScriptRoot\\..\\..").FullName.TrimEnd('\\') + '\\'`
489
+ )
490
+
491
+ // If the above didn't match (different whitespace/quoting), try simpler form
492
+ if (out === content) {
493
+ const marker = `$root = $PSScriptRoot.TrimEnd('\\') + '\\'`
494
+ if (content.includes(marker)) {
495
+ out = out.replace(
496
+ marker,
497
+ `$assetsRoot = $PSScriptRoot.TrimEnd('\\') + '\\'
498
+ $root = (Get-Item "$PSScriptRoot\\..\\..").FullName.TrimEnd('\\') + '\\'`
499
+ )
500
+ }
501
+ }
502
+
503
+ // 2. Add rootless fallback after $filePath = $resolved
504
+ const filePathLine = ' $filePath = $resolved'
505
+ if (out.includes(filePathLine)) {
506
+ out = out.replace(
507
+ filePathLine,
508
+ filePathLine + `
509
+ # rootless: fallback to .root/assets/ when file not found in project root
510
+ if ($relPath -ne '' -and -not (Test-Path $filePath)) {
511
+ $assetsCandidate = [System.IO.Path]::GetFullPath((Join-Path $assetsRoot $relPath))
512
+ if (Test-Path $assetsCandidate) { $filePath = $assetsCandidate }
513
+ }`
514
+ )
515
+ }
516
+
517
+ // 3. Fix 404 page lookup to also check $assetsRoot
518
+ const notFoundLine = ' $candidate = Join-Path $root $filename'
519
+ if (out.includes(notFoundLine)) {
520
+ out = out.replace(
521
+ notFoundLine,
522
+ notFoundLine + `
523
+ if (-not (Test-Path $candidate -PathType Leaf)) { $candidate = Join-Path $assetsRoot $filename }`
524
+ )
525
+ }
526
+
527
+ return out === content ? null : out
528
+ }
529
+
462
530
  /**
463
531
  * Add "prepare": "rootless prepare --yes" to project's package.json.
464
532
  */
@@ -492,5 +560,6 @@ export {
492
560
  NEVER_MIGRATE,
493
561
  ROOT_REQUIRED_PATTERNS,
494
562
  WEB_ASSET_PATTERNS,
563
+ patchServerScript,
495
564
  }
496
565
 
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import { createWatcher } from './watch/watcher.js'
5
5
  import { createDirtySet } from './watch/dirtySet.js'
6
6
  import { runIncrementalRegeneration } from './watch/incrementalRegeneration.js'
7
7
  import { resolveContainerPath } from './core/pathResolver.js'
8
+ import { resolveStaticFile, getServeDirs } from './serve/index.js'
8
9
 
9
10
  async function prepare(options = {}) {
10
11
  return preparePipeline(options)
@@ -36,4 +37,4 @@ async function watch(options = {}) {
36
37
  }
37
38
  }
38
39
 
39
- export { prepare, watch, clean }
40
+ export { prepare, watch, clean, resolveStaticFile, getServeDirs }
@@ -0,0 +1,74 @@
1
+ /*-------- Public serve API — for use in Tandem Sites and other dev servers --------*/
2
+
3
+ import path from 'node:path'
4
+ import { stat, readFile } from 'node:fs/promises'
5
+
6
+ /**
7
+ * Resolves a URL path to a real file on disk.
8
+ *
9
+ * Search order (first match wins):
10
+ * [1] projectRoot — real files/folders in project root
11
+ * [2] .root/assets/ — files migrated to rootless container
12
+ * [3] .root/env/ — .env files in rootless container
13
+ *
14
+ * Also tries index.html / index.htm for directory requests.
15
+ *
16
+ * @param {string} requestPath — e.g. '/viewer/app/code/app.js' or '/'
17
+ * @param {string} projectRoot — absolute path to the project root
18
+ * @returns {Promise<string|null>} absolute path to the file, or null if not found
19
+ */
20
+ async function resolveStaticFile(requestPath, projectRoot) {
21
+ const containerPath = path.join(projectRoot, '.root')
22
+ const assetsDir = path.join(containerPath, 'assets')
23
+ const envDir = path.join(containerPath, 'env')
24
+ const searchDirs = [projectRoot, assetsDir, envDir]
25
+
26
+ const rel = decodeURIComponent(requestPath).replace(/^\//, '') || ''
27
+
28
+ for (const dir of searchDirs) {
29
+ // Block access to .root internals when searching from projectRoot
30
+ if (dir === projectRoot && rel.startsWith('.root')) continue
31
+
32
+ const candidates = rel === ''
33
+ ? [
34
+ path.join(dir, 'index.html'),
35
+ path.join(dir, 'index.htm'),
36
+ ]
37
+ : [
38
+ path.join(dir, rel),
39
+ path.join(dir, rel, 'index.html'),
40
+ path.join(dir, rel, 'index.htm'),
41
+ ]
42
+
43
+ for (const candidate of candidates) {
44
+ try {
45
+ if ((await stat(candidate)).isFile()) return candidate
46
+ } catch {
47
+ // not found, try next
48
+ }
49
+ }
50
+ }
51
+
52
+ return null
53
+ }
54
+
55
+ /**
56
+ * Returns the ordered list of directories that rootless serves from.
57
+ * Useful for configuring a static server manually.
58
+ *
59
+ * @param {string} projectRoot
60
+ * @returns {{ projectRoot: string, assetsDir: string, envDir: string, searchDirs: string[] }}
61
+ */
62
+ function getServeDirs(projectRoot) {
63
+ const containerPath = path.join(projectRoot, '.root')
64
+ const assetsDir = path.join(containerPath, 'assets')
65
+ const envDir = path.join(containerPath, 'env')
66
+ return {
67
+ projectRoot,
68
+ assetsDir,
69
+ envDir,
70
+ searchDirs: [projectRoot, assetsDir, envDir],
71
+ }
72
+ }
73
+
74
+ export { resolveStaticFile, getServeDirs }