rootless-config 1.2.0 → 1.4.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 +14 -16
- package/src/core/scriptPatcher.js +332 -82
package/package.json
CHANGED
|
@@ -5,22 +5,21 @@ import { readdir, unlink, readFile } from 'node:fs/promises'
|
|
|
5
5
|
import { createLogger } from '../../utils/logger.js'
|
|
6
6
|
import { fileExists, ensureDir, atomicWrite, readJsonFile } from '../../utils/fsUtils.js'
|
|
7
7
|
import { confirm } from '../../utils/prompt.js'
|
|
8
|
-
import { patchPackageScripts } from '../../core/scriptPatcher.js'
|
|
8
|
+
import { patchPackageScripts, isRootRequired, isPatchable, isWebAsset, NEVER_MIGRATE } from '../../core/scriptPatcher.js'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
10
|
+
// A file is migratable when rootless knows what to do with it:
|
|
11
|
+
// - isRootRequired → must be in root (will be copied there by prepare)
|
|
12
|
+
// - isPatchable → can be redirected via --config flag in package.json scripts
|
|
13
|
+
// Anything that's NEVER_MIGRATE (lock files, package.json) is excluded.
|
|
14
14
|
|
|
15
15
|
async function findMigratableFiles(projectRoot) {
|
|
16
16
|
const entries = await readdir(projectRoot, { withFileTypes: true })
|
|
17
17
|
return entries
|
|
18
|
-
.filter(e =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
.filter(e => e.name !== 'package.json')
|
|
18
|
+
.filter(e => {
|
|
19
|
+
if (!e.isFile()) return false
|
|
20
|
+
if (NEVER_MIGRATE.has(e.name)) return false
|
|
21
|
+
return isRootRequired(e.name) || isPatchable(e.name)
|
|
22
|
+
})
|
|
24
23
|
.map(e => e.name)
|
|
25
24
|
.sort()
|
|
26
25
|
}
|
|
@@ -67,19 +66,18 @@ export default {
|
|
|
67
66
|
|
|
68
67
|
for (const name of candidates) {
|
|
69
68
|
const src = path.join(projectRoot, name)
|
|
70
|
-
const isEnv =
|
|
71
|
-
const
|
|
69
|
+
const isEnv = /^\.env/.test(name)
|
|
70
|
+
const destSubdir = isEnv ? 'env' : (isWebAsset(name) ? 'assets' : 'configs')
|
|
71
|
+
const destDir = path.join(containerPath, destSubdir)
|
|
72
72
|
|
|
73
73
|
await ensureDir(destDir)
|
|
74
74
|
const content = await readFile(src, 'utf8')
|
|
75
75
|
await atomicWrite(path.join(destDir, name), content)
|
|
76
76
|
|
|
77
77
|
if (isCleanMode) {
|
|
78
|
-
// Clean mode: delete original — no proxy file in root
|
|
79
78
|
await unlink(src)
|
|
80
|
-
logger.success(`Moved to .root/${
|
|
79
|
+
logger.success(`Moved to .root/${destSubdir}/${name} (deleted from root)`)
|
|
81
80
|
} else {
|
|
82
|
-
// Proxy mode: replace original with re-export stub
|
|
83
81
|
const rel = path.relative(path.dirname(src), path.join(destDir, name)).replace(/\\/g, '/')
|
|
84
82
|
const relPath = rel.startsWith('.') ? rel : `./${rel}`
|
|
85
83
|
await atomicWrite(src, `export { default } from "${relPath}"\n`)
|
|
@@ -1,92 +1,331 @@
|
|
|
1
|
-
/*-------- Patches package.json scripts to use --config flags pointing to .root/ --------*/
|
|
1
|
+
/*-------- Patches package.json scripts to use --config flags pointing to .root/ --------*/
|
|
2
2
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { readdir } from 'node:fs/promises'
|
|
5
5
|
import { readJsonFile, writeJsonFile, fileExists } from '../utils/fsUtils.js'
|
|
6
6
|
|
|
7
|
+
// ─── Tool definitions ────────────────────────────────────────────────────────
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Tools that support a --config (or similar) flag.
|
|
9
|
-
*
|
|
10
|
-
* Ordered by specificity (longer
|
|
11
|
+
* Matching files will NOT be copied to root — scripts get patched instead.
|
|
12
|
+
* Ordered by command specificity (longer commands first to avoid partial matches).
|
|
11
13
|
*/
|
|
12
14
|
const TOOL_CONFIGS = [
|
|
13
|
-
|
|
14
|
-
{ cmd: 'vite
|
|
15
|
-
{ cmd: 'vite',
|
|
16
|
-
{ cmd: '
|
|
17
|
-
|
|
18
|
-
{ cmd: '
|
|
19
|
-
{ cmd: '
|
|
20
|
-
|
|
21
|
-
{ cmd: '
|
|
22
|
-
|
|
23
|
-
{ cmd: '
|
|
24
|
-
|
|
25
|
-
{ cmd: '
|
|
26
|
-
|
|
27
|
-
{ cmd: '
|
|
28
|
-
|
|
29
|
-
{ cmd: '
|
|
30
|
-
{ cmd: '
|
|
31
|
-
|
|
32
|
-
{ cmd: '
|
|
33
|
-
|
|
34
|
-
{ cmd: '
|
|
35
|
-
|
|
36
|
-
{ cmd: '
|
|
15
|
+
// Vite
|
|
16
|
+
{ cmd: 'vite build', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
|
|
17
|
+
{ cmd: 'vite preview', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
|
|
18
|
+
{ cmd: 'vite', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
|
|
19
|
+
// Vitest
|
|
20
|
+
{ cmd: 'vitest run', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
|
|
21
|
+
{ cmd: 'vitest', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
|
|
22
|
+
// ESLint (v8 flat config + legacy .eslintrc variants)
|
|
23
|
+
{ cmd: 'eslint', flag: '--config', files: ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', '.eslintrc', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.mjs', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml'] },
|
|
24
|
+
// Prettier
|
|
25
|
+
{ cmd: 'prettier', flag: '--config', files: ['prettier.config.js', 'prettier.config.cjs', 'prettier.config.mjs', '.prettierrc', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.json', '.prettierrc.yaml', '.prettierrc.yml'] },
|
|
26
|
+
// Stylelint
|
|
27
|
+
{ cmd: 'stylelint', flag: '--config', files: ['stylelint.config.js', 'stylelint.config.cjs', 'stylelint.config.mjs', '.stylelintrc', '.stylelintrc.js', '.stylelintrc.json', '.stylelintrc.yaml', '.stylelintrc.yml'] },
|
|
28
|
+
// Jest
|
|
29
|
+
{ cmd: 'jest', flag: '--config', files: ['jest.config.js', 'jest.config.cjs', 'jest.config.mjs', 'jest.config.ts', 'jest.config.json'] },
|
|
30
|
+
// Webpack
|
|
31
|
+
{ cmd: 'webpack serve', flag: '--config', files: ['webpack.config.js', 'webpack.config.cjs', 'webpack.config.mjs', 'webpack.config.ts', 'webpack.common.js', 'webpack.dev.js', 'webpack.prod.js', 'webpack.base.js', 'webpack.config.babel.js'] },
|
|
32
|
+
{ cmd: 'webpack', flag: '--config', files: ['webpack.config.js', 'webpack.config.cjs', 'webpack.config.mjs', 'webpack.config.ts', 'webpack.common.js', 'webpack.dev.js', 'webpack.prod.js', 'webpack.base.js', 'webpack.config.babel.js'] },
|
|
33
|
+
// Rollup
|
|
34
|
+
{ cmd: 'rollup', flag: '--config', files: ['rollup.config.js', 'rollup.config.mjs', 'rollup.config.cjs', 'rollup.config.ts'] },
|
|
35
|
+
// tsup
|
|
36
|
+
{ cmd: 'tsup', flag: '--config', files: ['tsup.config.js', 'tsup.config.ts', 'tsup.config.mjs'] },
|
|
37
|
+
// TypeScript compiler
|
|
38
|
+
{ cmd: 'tsc', flag: '--project', files: ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.app.json', 'tsconfig.build.json', 'tsconfig.node.json', 'tsconfig.spec.json', 'tsconfig.test.json', 'tsconfig.worker.json'] },
|
|
39
|
+
// Nodemon
|
|
40
|
+
{ cmd: 'nodemon', flag: '--config', files: ['nodemon.json', '.nodemonrc', 'nodemon.config.js'] },
|
|
41
|
+
// Babel
|
|
42
|
+
{ cmd: 'babel', flag: '--config-file', files: ['babel.config.js', 'babel.config.cjs', 'babel.config.mjs', 'babel.config.json', '.babelrc', '.babelrc.js', '.babelrc.cjs', '.babelrc.json'] },
|
|
43
|
+
// PostCSS
|
|
44
|
+
{ cmd: 'postcss', flag: '--config', files: ['postcss.config.js', 'postcss.config.cjs', 'postcss.config.mjs', 'postcss.config.ts'] },
|
|
45
|
+
// Tailwind CSS
|
|
46
|
+
{ cmd: 'tailwindcss', flag: '--config', files: ['tailwind.config.js', 'tailwind.config.cjs', 'tailwind.config.mjs', 'tailwind.config.ts'] },
|
|
47
|
+
// Mocha
|
|
48
|
+
{ cmd: 'mocha', flag: '--config', files: ['.mocharc.js', '.mocharc.cjs', '.mocharc.json', '.mocharc.yaml', '.mocharc.yml'] },
|
|
49
|
+
// nyc (Istanbul coverage)
|
|
50
|
+
{ cmd: 'nyc', flag: '--nycrc', files: ['.nycrc', '.nycrc.json', 'nyc.config.js', 'nyc.config.cjs'] },
|
|
51
|
+
// Cypress
|
|
52
|
+
{ cmd: 'cypress run', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts'] },
|
|
53
|
+
{ cmd: 'cypress open', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts'] },
|
|
54
|
+
// Playwright
|
|
55
|
+
{ cmd: 'playwright test', flag: '--config', files: ['playwright.config.js', 'playwright.config.ts'] },
|
|
56
|
+
// Astro
|
|
57
|
+
{ cmd: 'astro dev', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
|
|
58
|
+
{ cmd: 'astro build', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
|
|
59
|
+
{ cmd: 'astro preview', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
|
|
60
|
+
{ cmd: 'astro', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
|
|
61
|
+
// Biome
|
|
62
|
+
{ cmd: 'biome check', flag: '--config-path', files: ['biome.json'] },
|
|
63
|
+
{ cmd: 'biome lint', flag: '--config-path', files: ['biome.json'] },
|
|
64
|
+
{ cmd: 'biome format', flag: '--config-path', files: ['biome.json'] },
|
|
65
|
+
{ cmd: 'biome ci', flag: '--config-path', files: ['biome.json'] },
|
|
66
|
+
{ cmd: 'biome', flag: '--config-path', files: ['biome.json'] },
|
|
37
67
|
]
|
|
38
68
|
|
|
39
69
|
/**
|
|
40
70
|
* 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
71
|
*/
|
|
43
72
|
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'] },
|
|
73
|
+
{ cmd: 'pm2 start', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs', 'pm2.config.js'] },
|
|
74
|
+
{ cmd: 'pm2 restart', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs', 'pm2.config.js'] },
|
|
75
|
+
{ cmd: 'pm2 reload', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs', 'pm2.config.js'] },
|
|
76
|
+
{ cmd: 'karma start', files: ['karma.conf.js', 'karma.conf.ts'] },
|
|
77
|
+
{ cmd: 'karma', files: ['karma.conf.js', 'karma.conf.ts'] },
|
|
47
78
|
]
|
|
48
79
|
|
|
49
80
|
/**
|
|
50
81
|
* Files that MUST be physically present in the project root.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* but we keep an explicit list for clarity and migrate detection.
|
|
82
|
+
* Tools discover them by convention — no CLI redirect is possible.
|
|
83
|
+
* These are COPIED to root by `rootless prepare`.
|
|
54
84
|
*/
|
|
55
85
|
const ROOT_REQUIRED_FILES = new Set([
|
|
56
|
-
// Node
|
|
86
|
+
// Node / npm
|
|
87
|
+
'.npmrc',
|
|
88
|
+
// Node version managers
|
|
57
89
|
'.nvmrc', '.node-version',
|
|
58
90
|
// Package managers
|
|
59
|
-
'.
|
|
91
|
+
'.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs', 'pnpm-workspace.yaml',
|
|
92
|
+
// TypeScript (auto-discovered by ts-node, ts-jest, esbuild, IDEs, etc.)
|
|
93
|
+
'tsconfig.json', 'tsconfig.base.json', 'tsconfig.app.json', 'tsconfig.build.json',
|
|
94
|
+
'tsconfig.node.json', 'tsconfig.spec.json', 'tsconfig.test.json', 'tsconfig.worker.json',
|
|
95
|
+
'jsconfig.json',
|
|
60
96
|
// Editor
|
|
61
97
|
'.editorconfig',
|
|
62
|
-
// Git
|
|
63
|
-
'.gitignore', '.gitattributes', '.gitmodules',
|
|
64
|
-
//
|
|
65
|
-
'
|
|
66
|
-
//
|
|
67
|
-
'
|
|
98
|
+
// Git
|
|
99
|
+
'.gitignore', '.gitattributes', '.gitmodules', '.mailmap',
|
|
100
|
+
// Linting ignore files (auto-discovered, no --flag support)
|
|
101
|
+
'.eslintignore', '.prettierignore', '.stylelintignore',
|
|
102
|
+
// Parcel (auto-discovers .parcelrc from root)
|
|
103
|
+
'.parcelrc', 'parcel.config.js',
|
|
104
|
+
// Next.js (no --config flag support)
|
|
105
|
+
'next.config.js', 'next.config.mjs', 'next.config.ts',
|
|
106
|
+
'middleware.ts', 'middleware.js', 'instrumentation.ts', 'instrumentation.js',
|
|
107
|
+
// Nuxt (auto-discovered)
|
|
108
|
+
'nuxt.config.js', 'nuxt.config.ts',
|
|
109
|
+
// Vue CLI (auto-discovered)
|
|
110
|
+
'vue.config.js',
|
|
111
|
+
// Angular
|
|
112
|
+
'angular.json', 'proxy.conf.json',
|
|
113
|
+
// SvelteKit / Svelte (auto-discovered by Vite plugin from root)
|
|
114
|
+
'svelte.config.js', 'svelte.config.cjs', 'svelte.config.mjs',
|
|
115
|
+
// Remix (auto-discovered)
|
|
116
|
+
'remix.config.js', 'remix.config.mjs', 'remix.config.ts',
|
|
117
|
+
// Monorepo tools
|
|
118
|
+
'turbo.json', 'nx.json', 'workspace.json', 'project.json', 'lerna.json', 'rush.json',
|
|
119
|
+
// Build tools (auto-discovered from root)
|
|
120
|
+
'.swcrc', 'swc.config.js', 'rome.json',
|
|
121
|
+
// CSS tooling (auto-discovered by framework plugins)
|
|
122
|
+
'windicss.config.js', 'windicss.config.ts', 'unocss.config.js', 'unocss.config.ts',
|
|
68
123
|
// Browser targets
|
|
69
124
|
'.browserslistrc', 'browserslist',
|
|
70
|
-
//
|
|
71
|
-
'.
|
|
72
|
-
// Commitlint / lint-staged / release (auto-discovery only)
|
|
125
|
+
// Commit / git hooks
|
|
126
|
+
'.huskyrc', '.huskyrc.json', '.huskyrc.js', '.huskyrc.yaml', '.huskyrc.yml',
|
|
73
127
|
'commitlint.config.js', 'commitlint.config.cjs', 'commitlint.config.mjs',
|
|
74
|
-
'.commitlintrc', '.commitlintrc.js', '.commitlintrc.json',
|
|
128
|
+
'.commitlintrc', '.commitlintrc.js', '.commitlintrc.json', '.commitlintrc.yaml', '.commitlintrc.yml',
|
|
129
|
+
'lint-staged.config.js', 'lint-staged.config.cjs', 'lint-staged.config.mjs',
|
|
75
130
|
'.lintstagedrc', '.lintstagedrc.js', '.lintstagedrc.json', '.lintstagedrc.cjs',
|
|
76
|
-
|
|
77
|
-
'.
|
|
78
|
-
'
|
|
79
|
-
// Semantic
|
|
131
|
+
// Release automation
|
|
132
|
+
'release.config.js', 'release.config.cjs', 'semantic-release.config.js', '.semantic-release.json',
|
|
133
|
+
'.releaserc', '.releaserc.js', '.releaserc.json', '.releaserc.yml', '.releaserc.yaml',
|
|
134
|
+
// Semantic commit
|
|
80
135
|
'.czrc', '.cz.json',
|
|
81
|
-
//
|
|
82
|
-
'.
|
|
83
|
-
//
|
|
136
|
+
// Docker
|
|
137
|
+
'Dockerfile', '.dockerignore', 'docker-compose.yml', 'docker-compose.yaml',
|
|
138
|
+
// CI
|
|
139
|
+
'.gitlab-ci.yml', 'azure-pipelines.yml',
|
|
140
|
+
// Web assets (served directly from root by web servers)
|
|
84
141
|
'robots.txt', 'sitemap.xml', 'favicon.ico', 'manifest.json', 'site.webmanifest',
|
|
142
|
+
// Documentation
|
|
143
|
+
'README.md', 'LICENSE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'CODE_OF_CONDUCT.md',
|
|
144
|
+
'SECURITY.md', 'SUPPORT.md',
|
|
145
|
+
// Dependency management / update bots
|
|
146
|
+
'renovate.json', '.renovaterc', '.renovaterc.json',
|
|
147
|
+
// Misc
|
|
148
|
+
'.htaccess', 'Procfile',
|
|
149
|
+
// GitHub Pages
|
|
150
|
+
'.nojekyll', 'CNAME', 'CXNAME',
|
|
151
|
+
// Hosting platform
|
|
152
|
+
'_redirects', '_headers', 'vercel.json', 'netlify.toml', 'wrangler.toml', 'fly.toml',
|
|
153
|
+
// Status / health
|
|
154
|
+
'status.json', 'health.json',
|
|
155
|
+
// NGINX / server
|
|
156
|
+
'nginx.conf', 'nginx.config',
|
|
157
|
+
// Service workers
|
|
158
|
+
'sw.js', 'service-worker.js',
|
|
159
|
+
// Web entry points
|
|
160
|
+
'index.html', 'index.htm', 'index.php',
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
// ─── Pattern-based root-required detection ───────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Regex patterns for files that MUST be physically in the project root,
|
|
167
|
+
* matched against the basename only.
|
|
168
|
+
* Used for file families whose exact names can vary
|
|
169
|
+
* (e.g. icon-192.png, 404.htm, logo-dark.svg, server.ps1 …).
|
|
170
|
+
*/
|
|
171
|
+
const ROOT_REQUIRED_PATTERNS = [
|
|
172
|
+
// Environment files
|
|
173
|
+
/^\.env/,
|
|
174
|
+
// Docker variants
|
|
175
|
+
/^Dockerfile(\..+)?$/,
|
|
176
|
+
/^docker-compose.*\.(yml|yaml)$/,
|
|
177
|
+
// tsconfig variants
|
|
178
|
+
/^tsconfig.*\.json$/,
|
|
179
|
+
// GitHub Pages & hosting markers
|
|
180
|
+
/^\.nojekyll$/,
|
|
181
|
+
/^CNAME$/i,
|
|
182
|
+
/^CXNAME$/i,
|
|
183
|
+
// Error pages — 404.html, 404.htm, 404.md, 500.html …
|
|
184
|
+
/^[45]\d{2}\.(html?|php|md|txt)$/,
|
|
185
|
+
// Web entry points
|
|
186
|
+
/^index\.(html?|php)$/,
|
|
187
|
+
// index-style assets — index-ui.css, index.app.js, index.min.js …
|
|
188
|
+
/^index[-.].+\.(css|js|mjs)$/,
|
|
189
|
+
// Service workers
|
|
190
|
+
/^sw\.js$/,
|
|
191
|
+
/^service-worker.*\.js$/,
|
|
192
|
+
/^workbox-.*\.js$/,
|
|
193
|
+
// Web manifests
|
|
194
|
+
/^manifest.*\.json$/,
|
|
195
|
+
/^site\.webmanifest$/,
|
|
196
|
+
// Favicon & icons
|
|
197
|
+
/^favicon(\..+)?$/,
|
|
198
|
+
/^apple-touch-icon.*\.(png|jpg)$/,
|
|
199
|
+
/^icon.*\.(png|ico|svg|jpg|jpeg)$/,
|
|
200
|
+
/^android-chrome-.*\.(png|jpg)$/,
|
|
201
|
+
/^mstile-.*\.png$/,
|
|
202
|
+
/^browserconfig\.xml$/,
|
|
203
|
+
// Social & OG images
|
|
204
|
+
/^og[-_]image.*\.(png|jpg|jpeg|webp)$/,
|
|
205
|
+
/^twitter[-_]image.*\.(png|jpg|jpeg|webp)$/,
|
|
206
|
+
/^social[-_]image.*\.(png|jpg|jpeg|webp)$/,
|
|
207
|
+
/^opengraph.*\.(png|jpg|jpeg|webp)$/,
|
|
208
|
+
// Logo & branding
|
|
209
|
+
/^logo.*\.(png|svg|jpg|jpeg|webp)$/,
|
|
210
|
+
// Screenshots & splash screens
|
|
211
|
+
/^splash.*\.(png|jpg|jpeg)$/,
|
|
212
|
+
/^screenshot.*\.(png|jpg|jpeg)$/,
|
|
213
|
+
// Sitemaps & SEO
|
|
214
|
+
/^sitemap.*\.(xml|txt)$/,
|
|
215
|
+
// NGINX & server configs
|
|
216
|
+
/^nginx.*\.(conf|config)$/,
|
|
217
|
+
/\.nginx$/,
|
|
218
|
+
/^\.htaccess$/,
|
|
219
|
+
/^Procfile(\..+)?$/,
|
|
220
|
+
// Shell / batch scripts in root (server.ps1, server.run.cmd, start.sh …)
|
|
221
|
+
/\.ps1$/,
|
|
222
|
+
/\.sh$/,
|
|
223
|
+
/\.(bat|cmd)$/,
|
|
224
|
+
// Version manager dotfiles
|
|
225
|
+
/^\.nvmrc$/,
|
|
226
|
+
/^\.node-version$/,
|
|
227
|
+
// Git
|
|
228
|
+
/^\.gitignore$/,
|
|
229
|
+
/^\.gitattributes$/,
|
|
230
|
+
/^\.gitmodules$/,
|
|
231
|
+
/^CODEOWNERS$/,
|
|
232
|
+
// Docker misc
|
|
233
|
+
/^\.dockerignore$/,
|
|
234
|
+
// Documentation (any extension, any case)
|
|
235
|
+
/^README(\..+)?$/i,
|
|
236
|
+
/^CHANGELOG(\..+)?$/i,
|
|
237
|
+
/^OVERVIEW(\..+)?$/i,
|
|
238
|
+
/^LICENSE(\..+)?$/i,
|
|
239
|
+
/^CONTRIBUTING(\..+)?$/i,
|
|
240
|
+
/^CODE_OF_CONDUCT(\..+)?$/i,
|
|
241
|
+
/^SECURITY(\..+)?$/i,
|
|
242
|
+
/^SUPPORT(\..+)?$/i,
|
|
243
|
+
/^COPYING(\..+)?$/i,
|
|
244
|
+
/^AUTHORS(\..+)?$/i,
|
|
245
|
+
/^NOTICE(\..+)?$/i,
|
|
246
|
+
// Hosting platform
|
|
247
|
+
/^_redirects$/,
|
|
248
|
+
/^_headers$/,
|
|
249
|
+
/^vercel\.json$/,
|
|
250
|
+
/^netlify\.toml$/,
|
|
251
|
+
/^wrangler\.toml$/,
|
|
252
|
+
/^fly\.toml$/,
|
|
253
|
+
// Status / health check files
|
|
254
|
+
/^status\.json$/,
|
|
255
|
+
/^health\.json$/,
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Patterns for web assets served directly from the root by a web server.
|
|
260
|
+
* Files matching these go into .root/assets/ so they get copied to root on `prepare`.
|
|
261
|
+
*/
|
|
262
|
+
const WEB_ASSET_PATTERNS = [
|
|
263
|
+
/^favicon(\..+)?$/,
|
|
264
|
+
/^apple-touch-icon.*\.(png|jpg)$/,
|
|
265
|
+
/^icon.*\.(png|ico|svg|jpg|jpeg)$/,
|
|
266
|
+
/^android-chrome-.*\.(png|jpg)$/,
|
|
267
|
+
/^mstile-.*\.png$/,
|
|
268
|
+
/^browserconfig\.xml$/,
|
|
269
|
+
/^og[-_]image.*\.(png|jpg|jpeg|webp)$/,
|
|
270
|
+
/^twitter[-_]image.*\.(png|jpg|jpeg|webp)$/,
|
|
271
|
+
/^social[-_]image.*\.(png|jpg|jpeg|webp)$/,
|
|
272
|
+
/^opengraph.*\.(png|jpg|jpeg|webp)$/,
|
|
273
|
+
/^logo.*\.(png|svg|jpg|jpeg|webp)$/,
|
|
274
|
+
/^splash.*\.(png|jpg|jpeg)$/,
|
|
275
|
+
/^screenshot.*\.(png|jpg|jpeg)$/,
|
|
276
|
+
/^[45]\d{2}\.(html?|php|md|txt)$/,
|
|
277
|
+
/^index\.(html?|php)$/,
|
|
278
|
+
/^index[-.].+\.(css|js|mjs)$/,
|
|
279
|
+
/^sw\.js$/,
|
|
280
|
+
/^service-worker.*\.js$/,
|
|
281
|
+
/^workbox-.*\.js$/,
|
|
282
|
+
/^manifest.*\.json$/,
|
|
283
|
+
/^site\.webmanifest$/,
|
|
284
|
+
/^sitemap.*\.(xml|txt)$/,
|
|
285
|
+
/^robots\.txt$/,
|
|
286
|
+
/^status\.json$/,
|
|
287
|
+
/^health\.json$/,
|
|
288
|
+
/^_redirects$/,
|
|
289
|
+
/^_headers$/,
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Lock files and core manifests that should NEVER be migrated.
|
|
294
|
+
* They must always live in the project root and are managed by package managers.
|
|
295
|
+
*/
|
|
296
|
+
const NEVER_MIGRATE = new Set([
|
|
297
|
+
'package.json',
|
|
298
|
+
'package-lock.json',
|
|
299
|
+
'yarn.lock',
|
|
300
|
+
'pnpm-lock.yaml',
|
|
301
|
+
'npm-shrinkwrap.json',
|
|
85
302
|
])
|
|
86
303
|
|
|
304
|
+
// ─── Helper functions ────────────────────────────────────────────────────────
|
|
305
|
+
|
|
87
306
|
/**
|
|
88
|
-
* Returns
|
|
89
|
-
*
|
|
307
|
+
* Returns all config file names known to rootless across all categories.
|
|
308
|
+
* Excludes NEVER_MIGRATE entries. Used by the migrate command.
|
|
309
|
+
*/
|
|
310
|
+
function getAllKnownFiles() {
|
|
311
|
+
const all = new Set()
|
|
312
|
+
for (const tool of TOOL_CONFIGS) {
|
|
313
|
+
for (const f of tool.files) all.add(f)
|
|
314
|
+
}
|
|
315
|
+
for (const tool of POSITIONAL_TOOLS) {
|
|
316
|
+
for (const f of tool.files) all.add(f)
|
|
317
|
+
}
|
|
318
|
+
for (const f of ROOT_REQUIRED_FILES) {
|
|
319
|
+
all.add(f)
|
|
320
|
+
}
|
|
321
|
+
for (const f of NEVER_MIGRATE) {
|
|
322
|
+
all.delete(f)
|
|
323
|
+
}
|
|
324
|
+
return all
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Returns true if the file can be redirected via CLI flags (not copied to root).
|
|
90
329
|
*/
|
|
91
330
|
function isPatchable(filename) {
|
|
92
331
|
return (
|
|
@@ -96,23 +335,30 @@ function isPatchable(filename) {
|
|
|
96
335
|
}
|
|
97
336
|
|
|
98
337
|
/**
|
|
99
|
-
* Returns true if the file must
|
|
338
|
+
* Returns true if the file must be physically present in the project root.
|
|
339
|
+
* Checks exact names first, then the pattern registry.
|
|
100
340
|
*/
|
|
101
341
|
function isRootRequired(filename) {
|
|
342
|
+
if (NEVER_MIGRATE.has(filename)) return false
|
|
102
343
|
if (ROOT_REQUIRED_FILES.has(filename)) return true
|
|
103
|
-
|
|
104
|
-
if (
|
|
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
|
|
344
|
+
if (ROOT_REQUIRED_PATTERNS.some(p => p.test(filename))) return true
|
|
345
|
+
if (!isPatchable(filename)) return true // unknown file → safe default: copy to root
|
|
109
346
|
return false
|
|
110
347
|
}
|
|
111
348
|
|
|
112
349
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
350
|
+
* Returns true if the file is a web asset that should be served
|
|
351
|
+
* directly from the project root by a web server (images, HTML, SW, manifest …).
|
|
352
|
+
* These land in .root/assets/ and are copied to root on `prepare`.
|
|
353
|
+
*/
|
|
354
|
+
function isWebAsset(filename) {
|
|
355
|
+
return WEB_ASSET_PATTERNS.some(p => p.test(filename))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Config map building ─────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Build a map of { cmd → { path, flag, positional } } based on files in configsDir.
|
|
116
362
|
*/
|
|
117
363
|
async function buildConfigMap(configsDir, projectRoot) {
|
|
118
364
|
let files = []
|
|
@@ -124,10 +370,8 @@ async function buildConfigMap(configsDir, projectRoot) {
|
|
|
124
370
|
|
|
125
371
|
const map = {}
|
|
126
372
|
|
|
127
|
-
// Flag-based tools
|
|
128
373
|
for (const tool of TOOL_CONFIGS) {
|
|
129
|
-
if (!tool.files.length) continue
|
|
130
|
-
if (map[tool.cmd]) continue
|
|
374
|
+
if (!tool.files.length || map[tool.cmd]) continue
|
|
131
375
|
const found = tool.files.find(f => files.includes(f))
|
|
132
376
|
if (!found) continue
|
|
133
377
|
const abs = path.join(configsDir, found)
|
|
@@ -135,39 +379,36 @@ async function buildConfigMap(configsDir, projectRoot) {
|
|
|
135
379
|
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: tool.flag, positional: false }
|
|
136
380
|
}
|
|
137
381
|
|
|
138
|
-
// Positional tools (pm2 etc.)
|
|
139
382
|
for (const tool of POSITIONAL_TOOLS) {
|
|
140
383
|
if (map[tool.cmd]) continue
|
|
141
384
|
const found = tool.files.find(f => files.includes(f))
|
|
142
385
|
if (!found) continue
|
|
143
386
|
const abs = path.join(configsDir, found)
|
|
144
387
|
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
|
|
145
|
-
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true,
|
|
388
|
+
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFiles: tool.files }
|
|
146
389
|
}
|
|
147
390
|
|
|
148
391
|
return map
|
|
149
392
|
}
|
|
150
393
|
|
|
394
|
+
// ─── Script patching ─────────────────────────────────────────────────────────
|
|
395
|
+
|
|
151
396
|
/**
|
|
152
|
-
* Inject --config flag
|
|
153
|
-
* Handles semicolons, &&, || (multi-command scripts).
|
|
397
|
+
* Inject --config flag (or replace positional arg) in a single script string.
|
|
154
398
|
*/
|
|
155
399
|
function patchScriptString(script, configMap) {
|
|
156
|
-
// Sort by command length descending to avoid partial matches
|
|
157
400
|
const sorted = Object.entries(configMap).sort((a, b) => b[0].length - a[0].length)
|
|
158
401
|
|
|
159
402
|
let result = script
|
|
160
403
|
for (const [cmd, info] of sorted) {
|
|
161
404
|
if (info.positional) {
|
|
162
|
-
|
|
163
|
-
for (const file of (POSITIONAL_TOOLS.find(t => t.cmd === cmd)?.files ?? [])) {
|
|
405
|
+
for (const file of (info.originalFiles ?? [])) {
|
|
164
406
|
const escapedFile = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
165
407
|
const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
|
|
166
408
|
const regex = new RegExp(`(${escapedCmd})\\s+${escapedFile}`, 'g')
|
|
167
409
|
result = result.replace(regex, `$1 ${info.path}`)
|
|
168
410
|
}
|
|
169
411
|
} else {
|
|
170
|
-
// Flag-based: insert --config <path> after command if not already present
|
|
171
412
|
const flag = info.flag
|
|
172
413
|
const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
|
|
173
414
|
const escapedFlag = flag.replace(/-/g, '\\-')
|
|
@@ -203,16 +444,12 @@ async function patchPackageScripts(projectRoot, configsDir, logger) {
|
|
|
203
444
|
}
|
|
204
445
|
}
|
|
205
446
|
|
|
206
|
-
if (changed)
|
|
207
|
-
await writeJsonFile(pkgPath, pkg)
|
|
208
|
-
}
|
|
447
|
+
if (changed) await writeJsonFile(pkgPath, pkg)
|
|
209
448
|
return changed
|
|
210
449
|
}
|
|
211
450
|
|
|
212
451
|
/**
|
|
213
452
|
* Add "prepare": "rootless prepare --yes" to project's package.json.
|
|
214
|
-
* If prepare already exists and doesn't include rootless, prepends to it.
|
|
215
|
-
* Returns the final prepare script value.
|
|
216
453
|
*/
|
|
217
454
|
async function addPrepareHook(projectRoot) {
|
|
218
455
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
@@ -232,4 +469,17 @@ async function addPrepareHook(projectRoot) {
|
|
|
232
469
|
return pkg.scripts.prepare
|
|
233
470
|
}
|
|
234
471
|
|
|
235
|
-
export {
|
|
472
|
+
export {
|
|
473
|
+
patchPackageScripts,
|
|
474
|
+
addPrepareHook,
|
|
475
|
+
buildConfigMap,
|
|
476
|
+
patchScriptString,
|
|
477
|
+
isPatchable,
|
|
478
|
+
isRootRequired,
|
|
479
|
+
isWebAsset,
|
|
480
|
+
getAllKnownFiles,
|
|
481
|
+
NEVER_MIGRATE,
|
|
482
|
+
ROOT_REQUIRED_PATTERNS,
|
|
483
|
+
WEB_ASSET_PATTERNS,
|
|
484
|
+
}
|
|
485
|
+
|