rootless-config 1.1.0 → 1.2.0
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 +1 -1
- package/src/cli/commands/migrate.js +8 -2
- package/src/core/cliWrapper.js +36 -10
- package/src/core/scriptPatcher.js +135 -27
package/package.json
CHANGED
|
@@ -9,12 +9,18 @@ import { patchPackageScripts } from '../../core/scriptPatcher.js'
|
|
|
9
9
|
|
|
10
10
|
const CONFIG_PATTERN = /\.(config|rc)\.(js|ts|mjs|cjs|json)$|\.eslintrc$|\.babelrc$|tsconfig.*\.json$/
|
|
11
11
|
const ENV_PATTERN = /^\.env/
|
|
12
|
+
// Dot-files and special files that must be in root — auto-detected
|
|
13
|
+
const DOTFILE_PATTERN = /^\.(nvmrc|node-version|npmrc|yarnrc|yarnrc\.yml|pnpmfile\.cjs|editorconfig|gitignore|gitattributes|dockerignore|browserslistrc|prettierignore|eslintignore|stylelintignore|czrc|cz\.json|commitlintrc.*|lintstagedrc.*|releaserc.*)$|^(Dockerfile.*|Procfile|browserslist)$/
|
|
12
14
|
|
|
13
15
|
async function findMigratableFiles(projectRoot) {
|
|
14
16
|
const entries = await readdir(projectRoot, { withFileTypes: true })
|
|
15
17
|
return entries
|
|
16
|
-
.filter(e => e.isFile() && (
|
|
17
|
-
|
|
18
|
+
.filter(e => e.isFile() && (
|
|
19
|
+
CONFIG_PATTERN.test(e.name) ||
|
|
20
|
+
ENV_PATTERN.test(e.name) ||
|
|
21
|
+
DOTFILE_PATTERN.test(e.name)
|
|
22
|
+
))
|
|
23
|
+
.filter(e => e.name !== 'package.json')
|
|
18
24
|
.map(e => e.name)
|
|
19
25
|
.sort()
|
|
20
26
|
}
|
package/src/core/cliWrapper.js
CHANGED
|
@@ -13,7 +13,7 @@ import { readManifest, removeFromManifest } from './generatedManifest.js'
|
|
|
13
13
|
import { removeHash } from './fileHashCache.js'
|
|
14
14
|
import { resolveProjectRoot, resolveContainerPath, setContainerOverride } from './pathResolver.js'
|
|
15
15
|
import { fileExists, removeFile } from '../utils/fsUtils.js'
|
|
16
|
-
import { patchPackageScripts } from './scriptPatcher.js'
|
|
16
|
+
import { patchPackageScripts, isRootRequired } from './scriptPatcher.js'
|
|
17
17
|
import { ValidationError } from '../types/errors.js'
|
|
18
18
|
|
|
19
19
|
async function loadConfig(containerPath) {
|
|
@@ -50,28 +50,54 @@ async function preparePipeline(options = {}) {
|
|
|
50
50
|
const containerPath = await resolveContainerPath()
|
|
51
51
|
const config = ctx.container.manifest ?? {}
|
|
52
52
|
const graph = await getGraph(config)
|
|
53
|
-
const isCleanMode = (config.mode ?? '
|
|
53
|
+
const isCleanMode = (config.mode ?? 'clean') === 'clean'
|
|
54
54
|
|
|
55
55
|
const projectRoot = await resolveProjectRoot()
|
|
56
56
|
|
|
57
57
|
if (isCleanMode) {
|
|
58
|
-
|
|
59
|
-
const envDir = path.join(containerPath, 'env')
|
|
58
|
+
const envDir = path.join(containerPath, 'env')
|
|
60
59
|
const configsDir = path.join(containerPath, 'configs')
|
|
60
|
+
const assetsDir = path.join(containerPath, 'assets')
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const copyEntries = []
|
|
63
|
+
|
|
64
|
+
// 1. env/* — all .env files always go to root
|
|
63
65
|
if (await fileExists(envDir)) {
|
|
64
|
-
const
|
|
65
|
-
for (const f of envFiles) {
|
|
66
|
+
for (const f of await readdir(envDir, { withFileTypes: true })) {
|
|
66
67
|
if (!f.isFile()) continue
|
|
67
68
|
const absPath = path.join(envDir, f.name)
|
|
68
69
|
const plugin = ctx.pluginRegistry.resolve(absPath)[0] ?? null
|
|
69
|
-
|
|
70
|
+
copyEntries.push({ source: absPath, target: path.join(projectRoot, f.name), mode: 'copy', plugin })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. assets/* — favicon, robots.txt, manifests etc. always go to root
|
|
75
|
+
if (await fileExists(assetsDir)) {
|
|
76
|
+
for (const f of await readdir(assetsDir, { withFileTypes: true })) {
|
|
77
|
+
if (!f.isFile()) continue
|
|
78
|
+
const absPath = path.join(assetsDir, f.name)
|
|
79
|
+
const plugin = ctx.pluginRegistry.resolve(absPath)[0] ?? null
|
|
80
|
+
copyEntries.push({ source: absPath, target: path.join(projectRoot, f.name), mode: 'copy', plugin })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. configs/* — split by whether the file can be redirected via CLI flags
|
|
85
|
+
// isRootRequired(name) = true → copy to root (tool can't be redirected: .nvmrc, .editorconfig, etc.)
|
|
86
|
+
// isRootRequired(name) = false → skip copy, only patch scripts below
|
|
87
|
+
if (await fileExists(configsDir)) {
|
|
88
|
+
for (const f of await readdir(configsDir, { withFileTypes: true })) {
|
|
89
|
+
if (!f.isFile()) continue
|
|
90
|
+
if (isRootRequired(f.name)) {
|
|
91
|
+
const absPath = path.join(configsDir, f.name)
|
|
92
|
+
const plugin = ctx.pluginRegistry.resolve(absPath)[0] ?? null
|
|
93
|
+
copyEntries.push({ source: absPath, target: path.join(projectRoot, f.name), mode: 'copy', plugin })
|
|
94
|
+
}
|
|
70
95
|
}
|
|
71
96
|
}
|
|
72
97
|
|
|
73
|
-
const results = await generateFiles(
|
|
98
|
+
const results = await generateFiles(copyEntries, options, ctx.logger)
|
|
74
99
|
|
|
100
|
+
// 4. Patch package.json scripts for all patchable tool configs (vite, eslint, jest, pm2, etc.)
|
|
75
101
|
const patched = await patchPackageScripts(projectRoot, configsDir, ctx.logger)
|
|
76
102
|
if (patched) ctx.logger.success('package.json scripts patched with --config flags')
|
|
77
103
|
else ctx.logger.debug('No script patching needed')
|
|
@@ -80,7 +106,7 @@ async function preparePipeline(options = {}) {
|
|
|
80
106
|
return results
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
// Proxy mode
|
|
109
|
+
// Proxy mode: generate proxy/copy for every file in .root/
|
|
84
110
|
const entries = await buildGenerationEntries(containerPath, graph, ctx)
|
|
85
111
|
const mapped = entries.map(e => ({
|
|
86
112
|
...e,
|
|
@@ -5,23 +5,110 @@ import { readdir } from 'node:fs/promises'
|
|
|
5
5
|
import { readJsonFile, writeJsonFile, fileExists } from '../utils/fsUtils.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Tools that support a --config (or similar) flag.
|
|
9
|
+
* These files can be REMOVED from root — scripts get patched instead.
|
|
9
10
|
* Ordered by specificity (longer command first to avoid partial matches).
|
|
10
11
|
*/
|
|
11
12
|
const TOOL_CONFIGS = [
|
|
12
|
-
{ cmd: 'vite build',
|
|
13
|
-
{ cmd: 'vite',
|
|
14
|
-
{ cmd: '
|
|
15
|
-
{ cmd: '
|
|
16
|
-
{ cmd: '
|
|
17
|
-
{ cmd: '
|
|
18
|
-
{ cmd: '
|
|
19
|
-
{ cmd: '
|
|
20
|
-
{ cmd: '
|
|
21
|
-
{ cmd: '
|
|
22
|
-
{ cmd: '
|
|
13
|
+
{ cmd: 'vite build', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
|
|
14
|
+
{ cmd: 'vite preview', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
|
|
15
|
+
{ cmd: 'vite', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
|
|
16
|
+
{ cmd: 'vitest run', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
|
|
17
|
+
{ cmd: 'vitest', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
|
|
18
|
+
{ cmd: 'eslint', flag: '--config', files: ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs'] },
|
|
19
|
+
{ cmd: 'prettier', flag: '--config', files: ['prettier.config.js', 'prettier.config.cjs', '.prettierrc.js', 'prettier.config.mjs'] },
|
|
20
|
+
{ cmd: 'jest', flag: '--config', files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs'] },
|
|
21
|
+
{ cmd: 'webpack serve', flag: '--config', files: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs'] },
|
|
22
|
+
{ cmd: 'webpack', flag: '--config', files: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs'] },
|
|
23
|
+
{ cmd: 'rollup', flag: '--config', files: ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs'] },
|
|
24
|
+
{ cmd: 'tsup', flag: '--config', files: ['tsup.config.js', 'tsup.config.ts', 'tsup.config.mjs'] },
|
|
25
|
+
{ cmd: 'tsc', flag: '--project', files: ['tsconfig.json', 'tsconfig.build.json', 'tsconfig.prod.json'] },
|
|
26
|
+
{ cmd: 'nodemon', flag: '--config', files: ['nodemon.json', '.nodemonrc', 'nodemon.config.js'] },
|
|
27
|
+
{ cmd: 'stylelint', flag: '--config', files: ['stylelint.config.js', 'stylelint.config.mjs', 'stylelint.config.cjs', '.stylelintrc.js', '.stylelintrc.json'] },
|
|
28
|
+
{ cmd: 'next build', flag: '--', files: [] }, // next doesn't support --config; handled separately
|
|
29
|
+
{ cmd: 'tailwindcss', flag: '--config', files: ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'] },
|
|
30
|
+
{ cmd: 'postcss', flag: '--config', files: ['postcss.config.js', 'postcss.config.mjs', 'postcss.config.cjs'] },
|
|
31
|
+
{ cmd: 'babel', flag: '--config-file', files: ['babel.config.js', 'babel.config.mjs', 'babel.config.cjs', 'babel.config.json'] },
|
|
32
|
+
{ cmd: 'mocha', flag: '--config', files: ['.mocharc.js', '.mocharc.cjs', '.mocharc.yml', '.mocharc.json', 'mocha.config.js'] },
|
|
33
|
+
{ cmd: 'nyc', flag: '--config', files: ['.nycrc', '.nycrc.json', 'nyc.config.js', 'nyc.config.cjs'] },
|
|
34
|
+
{ cmd: 'cypress run', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts', 'cypress.config.mjs'] },
|
|
35
|
+
{ cmd: 'cypress open', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts', 'cypress.config.mjs'] },
|
|
36
|
+
{ cmd: 'playwright test', flag: '--config', files: ['playwright.config.js', 'playwright.config.ts', 'playwright.config.mjs'] },
|
|
23
37
|
]
|
|
24
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Tools that use the config file as a POSITIONAL argument (not a --flag).
|
|
41
|
+
* These files can also be removed from root — the argument in the script is patched.
|
|
42
|
+
*/
|
|
43
|
+
const POSITIONAL_TOOLS = [
|
|
44
|
+
{ cmd: 'pm2 start', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs'] },
|
|
45
|
+
{ cmd: 'pm2 restart', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs'] },
|
|
46
|
+
{ cmd: 'pm2 reload', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs'] },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Files that MUST be physically present in the project root.
|
|
51
|
+
* These cannot be redirected via CLI flags — tools discover them by convention.
|
|
52
|
+
* Everything in .root/configs/ NOT matching TOOL_CONFIGS or POSITIONAL_TOOLS falls into this category,
|
|
53
|
+
* but we keep an explicit list for clarity and migrate detection.
|
|
54
|
+
*/
|
|
55
|
+
const ROOT_REQUIRED_FILES = new Set([
|
|
56
|
+
// Node.js version managers
|
|
57
|
+
'.nvmrc', '.node-version',
|
|
58
|
+
// Package managers
|
|
59
|
+
'.npmrc', '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs',
|
|
60
|
+
// Editor
|
|
61
|
+
'.editorconfig',
|
|
62
|
+
// Git (root discovery via git, these are auto-found)
|
|
63
|
+
'.gitignore', '.gitattributes', '.gitmodules',
|
|
64
|
+
// Docker
|
|
65
|
+
'Dockerfile', '.dockerignore',
|
|
66
|
+
// Process / deployment
|
|
67
|
+
'Procfile',
|
|
68
|
+
// Browser targets
|
|
69
|
+
'.browserslistrc', 'browserslist',
|
|
70
|
+
// Lint ignore files (eslint/prettier look for these automatically)
|
|
71
|
+
'.eslintignore', '.prettierignore', '.stylelintignore',
|
|
72
|
+
// Commitlint / lint-staged / release (auto-discovery only)
|
|
73
|
+
'commitlint.config.js', 'commitlint.config.cjs', 'commitlint.config.mjs',
|
|
74
|
+
'.commitlintrc', '.commitlintrc.js', '.commitlintrc.json',
|
|
75
|
+
'.lintstagedrc', '.lintstagedrc.js', '.lintstagedrc.json', '.lintstagedrc.cjs',
|
|
76
|
+
'lint-staged.config.js', 'lint-staged.config.mjs', 'lint-staged.config.cjs',
|
|
77
|
+
'.releaserc', '.releaserc.js', '.releaserc.json', '.releaserc.yml',
|
|
78
|
+
'release.config.js', 'release.config.cjs',
|
|
79
|
+
// Semantic tooling
|
|
80
|
+
'.czrc', '.cz.json',
|
|
81
|
+
// HTTPS / certs
|
|
82
|
+
'.htaccess',
|
|
83
|
+
// Web assets
|
|
84
|
+
'robots.txt', 'sitemap.xml', 'favicon.ico', 'manifest.json', 'site.webmanifest',
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true if the file can be redirected away from root via CLI flags.
|
|
89
|
+
* Files where this returns false MUST be copied to root.
|
|
90
|
+
*/
|
|
91
|
+
function isPatchable(filename) {
|
|
92
|
+
return (
|
|
93
|
+
TOOL_CONFIGS.some(t => t.files.includes(filename)) ||
|
|
94
|
+
POSITIONAL_TOOLS.some(t => t.files.includes(filename))
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns true if the file must always be physically present in root.
|
|
100
|
+
*/
|
|
101
|
+
function isRootRequired(filename) {
|
|
102
|
+
if (ROOT_REQUIRED_FILES.has(filename)) return true
|
|
103
|
+
// .env* patterns
|
|
104
|
+
if (/^\.env/.test(filename)) return true
|
|
105
|
+
// Dockerfile variants: Dockerfile.dev, Dockerfile.prod etc.
|
|
106
|
+
if (/^Dockerfile/.test(filename)) return true
|
|
107
|
+
// Not patchable and not in ROOT_REQUIRED_FILES → still copy to be safe
|
|
108
|
+
if (!isPatchable(filename)) return true
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
25
112
|
/**
|
|
26
113
|
* Build a map of { cmd → relative config path } based on what actually exists in configsDir.
|
|
27
114
|
* configsDir is the absolute path to .root/configs/
|
|
@@ -36,14 +123,28 @@ async function buildConfigMap(configsDir, projectRoot) {
|
|
|
36
123
|
}
|
|
37
124
|
|
|
38
125
|
const map = {}
|
|
126
|
+
|
|
127
|
+
// Flag-based tools
|
|
39
128
|
for (const tool of TOOL_CONFIGS) {
|
|
40
|
-
if (
|
|
129
|
+
if (!tool.files.length) continue
|
|
130
|
+
if (map[tool.cmd]) continue
|
|
131
|
+
const found = tool.files.find(f => files.includes(f))
|
|
132
|
+
if (!found) continue
|
|
133
|
+
const abs = path.join(configsDir, found)
|
|
134
|
+
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
|
|
135
|
+
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: tool.flag, positional: false }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Positional tools (pm2 etc.)
|
|
139
|
+
for (const tool of POSITIONAL_TOOLS) {
|
|
140
|
+
if (map[tool.cmd]) continue
|
|
41
141
|
const found = tool.files.find(f => files.includes(f))
|
|
42
142
|
if (!found) continue
|
|
43
143
|
const abs = path.join(configsDir, found)
|
|
44
144
|
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
|
|
45
|
-
map[tool.cmd] = rel.startsWith('.') ? rel : `./${rel}
|
|
145
|
+
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFile: found }
|
|
46
146
|
}
|
|
147
|
+
|
|
47
148
|
return map
|
|
48
149
|
}
|
|
49
150
|
|
|
@@ -52,20 +153,27 @@ async function buildConfigMap(configsDir, projectRoot) {
|
|
|
52
153
|
* Handles semicolons, &&, || (multi-command scripts).
|
|
53
154
|
*/
|
|
54
155
|
function patchScriptString(script, configMap) {
|
|
55
|
-
//
|
|
156
|
+
// Sort by command length descending to avoid partial matches
|
|
56
157
|
const sorted = Object.entries(configMap).sort((a, b) => b[0].length - a[0].length)
|
|
57
158
|
|
|
58
159
|
let result = script
|
|
59
|
-
for (const [cmd,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
160
|
+
for (const [cmd, info] of sorted) {
|
|
161
|
+
if (info.positional) {
|
|
162
|
+
// Replace filename argument directly: pm2 start ecosystem.config.cjs → pm2 start .root/...
|
|
163
|
+
for (const file of (POSITIONAL_TOOLS.find(t => t.cmd === cmd)?.files ?? [])) {
|
|
164
|
+
const escapedFile = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
165
|
+
const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
|
|
166
|
+
const regex = new RegExp(`(${escapedCmd})\\s+${escapedFile}`, 'g')
|
|
167
|
+
result = result.replace(regex, `$1 ${info.path}`)
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// Flag-based: insert --config <path> after command if not already present
|
|
171
|
+
const flag = info.flag
|
|
172
|
+
const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
|
|
173
|
+
const escapedFlag = flag.replace(/-/g, '\\-')
|
|
174
|
+
const regex = new RegExp(`((?:^|[;&|\\s]))(${escapedCmd})(?!\\s+${escapedFlag})`, 'g')
|
|
175
|
+
result = result.replace(regex, (_, pre, match) => `${pre}${match} ${flag} ${info.path}`)
|
|
176
|
+
}
|
|
69
177
|
}
|
|
70
178
|
return result
|
|
71
179
|
}
|
|
@@ -114,7 +222,7 @@ async function addPrepareHook(projectRoot) {
|
|
|
114
222
|
if (!pkg.scripts) pkg.scripts = {}
|
|
115
223
|
|
|
116
224
|
const existing = pkg.scripts.prepare ?? ''
|
|
117
|
-
if (existing.includes('rootless prepare')) return existing
|
|
225
|
+
if (existing.includes('rootless prepare')) return existing
|
|
118
226
|
|
|
119
227
|
pkg.scripts.prepare = existing
|
|
120
228
|
? `rootless prepare --yes && ${existing}`
|
|
@@ -124,4 +232,4 @@ async function addPrepareHook(projectRoot) {
|
|
|
124
232
|
return pkg.scripts.prepare
|
|
125
233
|
}
|
|
126
234
|
|
|
127
|
-
export { patchPackageScripts, addPrepareHook, buildConfigMap, patchScriptString }
|
|
235
|
+
export { patchPackageScripts, addPrepareHook, buildConfigMap, patchScriptString, isPatchable, isRootRequired }
|