@smoothbricks/cli 0.1.1 → 0.3.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.
Files changed (131) hide show
  1. package/README.md +161 -32
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +110 -10
  4. package/dist/devenv/index.d.ts +10 -0
  5. package/dist/devenv/index.d.ts.map +1 -0
  6. package/dist/devenv/index.js +27 -0
  7. package/dist/generate/index.d.ts +20 -0
  8. package/dist/generate/index.d.ts.map +1 -0
  9. package/dist/generate/index.js +47 -0
  10. package/dist/lib/cli-package.d.ts +4 -0
  11. package/dist/lib/cli-package.d.ts.map +1 -0
  12. package/dist/lib/cli-package.js +15 -0
  13. package/dist/lib/run.d.ts +6 -0
  14. package/dist/lib/run.d.ts.map +1 -1
  15. package/dist/lib/run.js +32 -0
  16. package/dist/lib/workspace.d.ts +25 -1
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +79 -17
  19. package/dist/monorepo/commit-msg.d.ts +8 -1
  20. package/dist/monorepo/commit-msg.d.ts.map +1 -1
  21. package/dist/monorepo/commit-msg.js +85 -7
  22. package/dist/monorepo/index.d.ts +19 -2
  23. package/dist/monorepo/index.d.ts.map +1 -1
  24. package/dist/monorepo/index.js +104 -16
  25. package/dist/monorepo/lockfile.d.ts.map +1 -1
  26. package/dist/monorepo/lockfile.js +9 -0
  27. package/dist/monorepo/managed-files.js +1 -1
  28. package/dist/monorepo/nx-sync.d.ts +2 -1
  29. package/dist/monorepo/nx-sync.d.ts.map +1 -1
  30. package/dist/monorepo/nx-sync.js +8 -2
  31. package/dist/monorepo/package-hygiene.d.ts +6 -2
  32. package/dist/monorepo/package-hygiene.d.ts.map +1 -1
  33. package/dist/monorepo/package-hygiene.js +12 -4
  34. package/dist/monorepo/package-policy.d.ts +13 -4
  35. package/dist/monorepo/package-policy.d.ts.map +1 -1
  36. package/dist/monorepo/package-policy.js +208 -113
  37. package/dist/monorepo/packed-package.d.ts +3 -0
  38. package/dist/monorepo/packed-package.d.ts.map +1 -1
  39. package/dist/monorepo/packed-package.js +68 -36
  40. package/dist/monorepo/packs/index.d.ts +20 -1
  41. package/dist/monorepo/packs/index.d.ts.map +1 -1
  42. package/dist/monorepo/packs/index.js +238 -19
  43. package/dist/monorepo/publish-workflow.d.ts +0 -4
  44. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  45. package/dist/monorepo/publish-workflow.js +7 -15
  46. package/dist/monorepo/tool-validation.d.ts +22 -0
  47. package/dist/monorepo/tool-validation.d.ts.map +1 -0
  48. package/dist/monorepo/tool-validation.js +399 -0
  49. package/dist/nx/index.d.ts +25 -0
  50. package/dist/nx/index.d.ts.map +1 -0
  51. package/dist/nx/index.js +112 -0
  52. package/dist/release/bootstrap-npm-packages.d.ts +19 -0
  53. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
  54. package/dist/release/bootstrap-npm-packages.js +58 -0
  55. package/dist/release/candidates.d.ts +11 -0
  56. package/dist/release/candidates.d.ts.map +1 -0
  57. package/dist/release/candidates.js +181 -0
  58. package/dist/release/core.d.ts +4 -1
  59. package/dist/release/core.d.ts.map +1 -1
  60. package/dist/release/core.js +76 -9
  61. package/dist/release/github-release.d.ts +1 -0
  62. package/dist/release/github-release.d.ts.map +1 -1
  63. package/dist/release/github-release.js +3 -3
  64. package/dist/release/index.d.ts +32 -1
  65. package/dist/release/index.d.ts.map +1 -1
  66. package/dist/release/index.js +488 -64
  67. package/dist/release/npm-auth.d.ts +0 -1
  68. package/dist/release/npm-auth.d.ts.map +1 -1
  69. package/dist/release/npm-auth.js +4 -16
  70. package/dist/release/orchestration.d.ts +11 -2
  71. package/dist/release/orchestration.d.ts.map +1 -1
  72. package/dist/release/orchestration.js +41 -3
  73. package/dist/release/publish-plan.js +1 -1
  74. package/managed/raw/git-format-staged.yml +54 -0
  75. package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
  76. package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
  77. package/managed/templates/github/workflows/ci.yml +4 -2
  78. package/package.json +15 -8
  79. package/src/cli.ts +153 -22
  80. package/src/devenv/index.test.ts +49 -0
  81. package/src/devenv/index.ts +38 -0
  82. package/src/generate/index.ts +92 -0
  83. package/src/lib/cli-package.ts +18 -0
  84. package/src/lib/run.ts +44 -0
  85. package/src/lib/workspace.ts +112 -17
  86. package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
  87. package/src/monorepo/commit-msg.test.ts +45 -0
  88. package/src/monorepo/commit-msg.ts +107 -7
  89. package/src/monorepo/index.ts +142 -16
  90. package/src/monorepo/lockfile.ts +9 -0
  91. package/src/monorepo/managed-files.ts +1 -1
  92. package/src/monorepo/nx-sync.ts +9 -2
  93. package/src/monorepo/package-hygiene.test.ts +34 -0
  94. package/src/monorepo/package-hygiene.ts +26 -4
  95. package/src/monorepo/package-policy.test.ts +1130 -0
  96. package/src/monorepo/package-policy.ts +237 -114
  97. package/src/monorepo/packed-package.ts +89 -37
  98. package/src/monorepo/packs/index.test.ts +189 -0
  99. package/src/monorepo/packs/index.ts +311 -20
  100. package/src/monorepo/publish-workflow.ts +10 -19
  101. package/src/monorepo/setup-test-tracing.test.ts +120 -0
  102. package/src/monorepo/tool-validation.test.ts +261 -0
  103. package/src/monorepo/tool-validation.ts +462 -0
  104. package/src/nx/index.test.ts +73 -0
  105. package/src/nx/index.ts +139 -0
  106. package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
  107. package/src/release/__tests__/candidates.test.ts +262 -0
  108. package/src/release/__tests__/core-properties.test.ts +6 -6
  109. package/src/release/__tests__/core-scenarios.test.ts +27 -20
  110. package/src/release/__tests__/core.test.ts +52 -5
  111. package/src/release/__tests__/fixture-repo.test.ts +90 -31
  112. package/src/release/__tests__/github-release.test.ts +22 -24
  113. package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
  114. package/src/release/__tests__/npm-auth.test.ts +12 -21
  115. package/src/release/__tests__/orchestration.test.ts +142 -12
  116. package/src/release/__tests__/publish-plan.test.ts +9 -3
  117. package/src/release/__tests__/retag-unpublished.test.ts +10 -10
  118. package/src/release/__tests__/trust-publisher.test.ts +165 -0
  119. package/src/release/bootstrap-npm-packages.ts +88 -0
  120. package/src/release/candidates.ts +225 -0
  121. package/src/release/core.ts +96 -10
  122. package/src/release/github-release.ts +3 -3
  123. package/src/release/index.ts +658 -74
  124. package/src/release/npm-auth.ts +4 -33
  125. package/src/release/orchestration.ts +60 -5
  126. package/src/release/publish-plan.ts +1 -1
  127. package/dist/nx-version-actions.cjs +0 -25
  128. package/dist/nx-version-actions.d.cts +0 -6
  129. package/dist/nx-version-actions.d.cts.map +0 -1
  130. package/managed/raw/.git-format-staged.yml +0 -47
  131. package/src/nx-version-actions.cts +0 -36
@@ -0,0 +1,225 @@
1
+ import { isRecord } from '../lib/json.js';
2
+ import type { ReleasePackageInfo } from './core.js';
3
+ import { releaseTag } from './core.js';
4
+
5
+ export interface AutoReleaseCandidateShell {
6
+ gitRefExists(ref: string): Promise<boolean>;
7
+ packageChangedFilesSince(ref: string, packagePath: string): Promise<string[]>;
8
+ packageJsonAtRef(ref: string, packagePath: string): Promise<Record<string, unknown> | null>;
9
+ currentPackageJson(packagePath: string): Promise<Record<string, unknown> | null>;
10
+ packageBuildInputPatterns(projectName: string, packagePath: string): Promise<string[]>;
11
+ packageHasHistory(packagePath: string): Promise<boolean>;
12
+ }
13
+
14
+ const releasableManifestKeys = [
15
+ 'author',
16
+ 'bin',
17
+ 'browser',
18
+ 'bugs',
19
+ 'cpu',
20
+ 'dependencies',
21
+ 'description',
22
+ 'engines',
23
+ 'exports',
24
+ 'files',
25
+ 'funding',
26
+ 'homepage',
27
+ 'imports',
28
+ 'keywords',
29
+ 'license',
30
+ 'main',
31
+ 'module',
32
+ 'name',
33
+ 'optionalDependencies',
34
+ 'os',
35
+ 'peerDependencies',
36
+ 'peerDependenciesMeta',
37
+ 'publishConfig',
38
+ 'repository',
39
+ 'sideEffects',
40
+ 'types',
41
+ 'typesVersions',
42
+ ] as const;
43
+
44
+ export async function autoReleaseCandidatePackages<Package extends ReleasePackageInfo>(
45
+ shell: AutoReleaseCandidateShell,
46
+ packages: Package[],
47
+ ): Promise<Package[]> {
48
+ const candidates: Package[] = [];
49
+ for (const pkg of packages) {
50
+ if (await isAutoReleaseCandidate(shell, pkg)) {
51
+ candidates.push(pkg);
52
+ }
53
+ }
54
+ return candidates;
55
+ }
56
+
57
+ async function isAutoReleaseCandidate<Package extends ReleasePackageInfo>(
58
+ shell: AutoReleaseCandidateShell,
59
+ pkg: Package,
60
+ ): Promise<boolean> {
61
+ const tagRef = `refs/tags/${releaseTag(pkg)}`;
62
+ if (await shell.gitRefExists(tagRef)) {
63
+ return packageHasReleasableChangesSince(shell, tagRef, pkg);
64
+ }
65
+ return !pkg.version.includes('-') && shell.packageHasHistory(pkg.path);
66
+ }
67
+
68
+ async function packageHasReleasableChangesSince<Package extends ReleasePackageInfo>(
69
+ shell: AutoReleaseCandidateShell,
70
+ ref: string,
71
+ pkg: Package,
72
+ ): Promise<boolean> {
73
+ const changedFiles = await shell.packageChangedFilesSince(ref, pkg.path);
74
+ if (changedFiles.length === 0) {
75
+ return false;
76
+ }
77
+ const currentManifest = await shell.currentPackageJson(pkg.path);
78
+ const buildInputPatterns = await shell.packageBuildInputPatterns(pkg.projectName, pkg.path);
79
+ for (const changedFile of changedFiles) {
80
+ if (changedFile === 'package.json') {
81
+ const previousManifest = await shell.packageJsonAtRef(ref, pkg.path);
82
+ if (!previousManifest || !currentManifest || releasableManifestChanged(previousManifest, currentManifest)) {
83
+ return true;
84
+ }
85
+ continue;
86
+ }
87
+ if (isReleasablePackagePath(changedFile, currentManifest, buildInputPatterns)) {
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+
94
+ function releasableManifestChanged(
95
+ previousManifest: Record<string, unknown>,
96
+ currentManifest: Record<string, unknown>,
97
+ ): boolean {
98
+ for (const key of releasableManifestKeys) {
99
+ if (!stableJsonEqual(previousManifest[key], currentManifest[key])) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+
106
+ function isReleasablePackagePath(
107
+ path: string,
108
+ manifest: Record<string, unknown> | null,
109
+ buildInputPatterns: string[],
110
+ ): boolean {
111
+ return (
112
+ isBuildInputPath(path, buildInputPatterns) || isPackageMetadataPath(path) || isManifestFilesPath(path, manifest)
113
+ );
114
+ }
115
+
116
+ function isBuildInputPath(path: string, patterns: string[]): boolean {
117
+ if (isReleaseIgnoredBuildInputPath(path)) {
118
+ return false;
119
+ }
120
+ let matched = false;
121
+ for (const pattern of patterns) {
122
+ const excluded = pattern.startsWith('!');
123
+ const rawPattern = excluded ? pattern.slice(1) : pattern;
124
+ if (matchesBuildInputPattern(path, rawPattern)) {
125
+ matched = !excluded;
126
+ }
127
+ }
128
+ return matched;
129
+ }
130
+
131
+ function matchesBuildInputPattern(path: string, pattern: string): boolean {
132
+ const normalized = pattern.replace(/^\.\//, '').replace(/\/$/, '');
133
+ if (!normalized) {
134
+ return false;
135
+ }
136
+ return globPatternToRegExp(normalized).test(path);
137
+ }
138
+
139
+ function globPatternToRegExp(pattern: string): RegExp {
140
+ let source = '^';
141
+ for (let index = 0; index < pattern.length; index += 1) {
142
+ const char = pattern[index];
143
+ if (char === '*') {
144
+ if (pattern[index + 1] === '*') {
145
+ if (pattern[index + 2] === '/') {
146
+ source += '(?:.*/)?';
147
+ index += 2;
148
+ } else {
149
+ source += '.*';
150
+ index += 1;
151
+ }
152
+ } else {
153
+ source += '[^/]*';
154
+ }
155
+ } else {
156
+ source += escapeRegExpChar(char);
157
+ }
158
+ }
159
+ return new RegExp(`${source}$`);
160
+ }
161
+
162
+ function escapeRegExpChar(char: string | undefined): string {
163
+ if (!char) {
164
+ return '';
165
+ }
166
+ return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
167
+ }
168
+
169
+ function isReleaseIgnoredBuildInputPath(path: string): boolean {
170
+ return (
171
+ path === 'package.json' ||
172
+ path === 'tsconfig.test.json' ||
173
+ path.includes('/__tests__/') ||
174
+ path.endsWith('.test.ts') ||
175
+ path.endsWith('.test.tsx') ||
176
+ path.endsWith('.spec.ts') ||
177
+ path.endsWith('.spec.tsx')
178
+ );
179
+ }
180
+
181
+ function isPackageMetadataPath(path: string): boolean {
182
+ return /^(README|LICENSE|CHANGELOG)(\.|$)/.test(path);
183
+ }
184
+
185
+ function isManifestFilesPath(path: string, manifest: Record<string, unknown> | null): boolean {
186
+ const files = manifest?.files;
187
+ if (!Array.isArray(files)) {
188
+ return false;
189
+ }
190
+ for (const entry of files) {
191
+ if (typeof entry !== 'string' || entry.length === 0 || entry.startsWith('!')) {
192
+ continue;
193
+ }
194
+ if (matchesManifestFilesEntry(path, entry)) {
195
+ return true;
196
+ }
197
+ }
198
+ return false;
199
+ }
200
+
201
+ function matchesManifestFilesEntry(path: string, entry: string): boolean {
202
+ const normalized = entry.replace(/^\.\//, '').replace(/\/$/, '');
203
+ if (!normalized || normalized.includes('*')) {
204
+ return false;
205
+ }
206
+ return path === normalized || path.startsWith(`${normalized}/`);
207
+ }
208
+
209
+ function stableJsonEqual(left: unknown, right: unknown): boolean {
210
+ return JSON.stringify(stableJson(left)) === JSON.stringify(stableJson(right));
211
+ }
212
+
213
+ function stableJson(value: unknown): unknown {
214
+ if (Array.isArray(value)) {
215
+ return value.map(stableJson);
216
+ }
217
+ if (isRecord(value)) {
218
+ const normalized: Record<string, unknown> = {};
219
+ for (const key of Object.keys(value).sort()) {
220
+ normalized[key] = stableJson(value[key]);
221
+ }
222
+ return normalized;
223
+ }
224
+ return value;
225
+ }
@@ -1,5 +1,6 @@
1
1
  export interface ReleasePackageInfo {
2
2
  name: string;
3
+ projectName: string;
3
4
  path: string;
4
5
  version: string;
5
6
  }
@@ -43,10 +44,18 @@ export interface ReleasePlanningShell {
43
44
  durableTagState(pkg: ReleasePackageInfo, tag: string): Promise<DurableTagState>;
44
45
  }
45
46
 
46
- export function releaseTag(pkg: Pick<ReleasePackageInfo, 'name' | 'version'>): string {
47
+ export function releaseTag(pkg: Pick<ReleasePackageInfo, 'projectName' | 'version'>): string {
48
+ return `${pkg.projectName}@${pkg.version}`;
49
+ }
50
+
51
+ export function legacyReleaseTag(pkg: Pick<ReleasePackageInfo, 'name' | 'version'>): string {
47
52
  return `${pkg.name}@${pkg.version}`;
48
53
  }
49
54
 
55
+ export function releaseTagAliases(pkg: Pick<ReleasePackageInfo, 'name' | 'projectName' | 'version'>): string[] {
56
+ return [...new Set([releaseTag(pkg), legacyReleaseTag(pkg)])];
57
+ }
58
+
50
59
  export function npmDistTagForVersion(version: string): string {
51
60
  return version.includes('-') ? 'next' : 'latest';
52
61
  }
@@ -56,7 +65,7 @@ export function releasePackageForTag<Package extends Omit<ReleasePackageInfo, 'v
56
65
  tag: string,
57
66
  ): { pkg: Package; version: string } | null {
58
67
  for (const pkg of packages) {
59
- const prefix = `${pkg.name}@`;
68
+ const prefix = `${pkg.projectName}@`;
60
69
  if (tag.startsWith(prefix)) {
61
70
  const version = tag.slice(prefix.length);
62
71
  if (!version) {
@@ -84,13 +93,14 @@ export async function collectOwnedReleaseTagRecords<Package extends Omit<Release
84
93
  ref: string,
85
94
  shell: ReleasePlanningShell,
86
95
  ): Promise<Array<ReleaseTagRecord<Package & { version: string }>>> {
87
- const records: Array<ReleaseTagRecord<Package & { version: string }>> = [];
88
- const completedPackages = new Set<string>();
96
+ const candidates: Array<{
97
+ tag: GitReleaseTagInfo;
98
+ pkg: Package & { version: string };
99
+ }> = [];
100
+ const durableStates = new BoundedPromiseRunner(8);
101
+
89
102
  for (const tag of await shell.listReleaseTagsByCreatorDate()) {
90
103
  const match = releasePackageForTag(packages, tag.name);
91
- if (match && completedPackages.has(match.pkg.name)) {
92
- continue;
93
- }
94
104
  if (!match || !(await shell.isAncestor(tag.sha, ref))) {
95
105
  continue;
96
106
  }
@@ -103,15 +113,91 @@ export async function collectOwnedReleaseTagRecords<Package extends Omit<Release
103
113
  );
104
114
  }
105
115
  const pkg = { ...match.pkg, version: match.version };
106
- const state = await shell.durableTagState(pkg, tag.name);
107
- records.push(classifyReleaseTag({ tag: tag.name, sha: tag.sha, timestamp: tag.timestamp, pkg }, state));
116
+ candidates.push({ tag, pkg });
117
+ }
118
+
119
+ const records: Array<ReleaseTagRecord<Package & { version: string }>> = [];
120
+ const completedPackages = new Set<string>();
121
+ const activePackages = new Set<string>();
122
+ const states = new Map<number, Promise<DurableTagState>>();
123
+
124
+ // Run durable-state checks concurrently across packages, but do not query an
125
+ // older tag for the same package until its newer tag proves incomplete.
126
+ const scheduleFrom = (startIndex: number) => {
127
+ for (let index = startIndex; index < candidates.length; index += 1) {
128
+ if (states.size >= 8) {
129
+ return;
130
+ }
131
+ const candidate = candidates[index];
132
+ if (!candidate || states.has(index) || completedPackages.has(candidate.pkg.name)) {
133
+ continue;
134
+ }
135
+ if (activePackages.has(candidate.pkg.name)) {
136
+ continue;
137
+ }
138
+ activePackages.add(candidate.pkg.name);
139
+ states.set(
140
+ index,
141
+ durableStates.run(() => shell.durableTagState(candidate.pkg, candidate.tag.name)),
142
+ );
143
+ }
144
+ };
145
+
146
+ for (const candidate of candidates) {
147
+ const index = candidates.indexOf(candidate);
148
+ scheduleFrom(index);
149
+ if (completedPackages.has(candidate.pkg.name)) {
150
+ continue;
151
+ }
152
+ const state = await states.get(index);
153
+ if (!state) {
154
+ throw new Error(`Release tag ${candidate.tag.name} lost durable state planning.`);
155
+ }
156
+ activePackages.delete(candidate.pkg.name);
157
+ states.delete(index);
158
+ records.push(
159
+ classifyReleaseTag(
160
+ { tag: candidate.tag.name, sha: candidate.tag.sha, timestamp: candidate.tag.timestamp, pkg: candidate.pkg },
161
+ state,
162
+ ),
163
+ );
108
164
  if (state.npmPublished && state.githubReleaseExists) {
109
- completedPackages.add(pkg.name);
165
+ completedPackages.add(candidate.pkg.name);
110
166
  }
111
167
  }
112
168
  return records;
113
169
  }
114
170
 
171
+ class BoundedPromiseRunner {
172
+ private active = 0;
173
+ private readonly waiting: Array<() => void> = [];
174
+
175
+ constructor(private readonly limit: number) {}
176
+
177
+ async run<T>(operation: () => Promise<T>): Promise<T> {
178
+ await this.acquire();
179
+ try {
180
+ return await operation();
181
+ } finally {
182
+ this.release();
183
+ }
184
+ }
185
+
186
+ private async acquire(): Promise<void> {
187
+ if (this.active < this.limit) {
188
+ this.active += 1;
189
+ return;
190
+ }
191
+ await new Promise<void>((resolve) => this.waiting.push(resolve));
192
+ this.active += 1;
193
+ }
194
+
195
+ private release(): void {
196
+ this.active -= 1;
197
+ this.waiting.shift()?.();
198
+ }
199
+ }
200
+
115
201
  export function groupReleaseTargets<Package extends ReleasePackageInfo>(
116
202
  records: ReleaseTagRecord<Package>[],
117
203
  ): ReleaseTarget<Package>[] {
@@ -39,7 +39,7 @@ export async function renderNxProjectChangelogContents(input: RenderNxProjectCha
39
39
  nxRenderOnlyReleaseConfig,
40
40
  false,
41
41
  )(nxProjectChangelogArgs(input.pkg, input.previousTag, input.dryRun));
42
- return projectChangelogContents(result, input.pkg.name);
42
+ return projectChangelogContents(result, input.pkg.projectName);
43
43
  });
44
44
  }
45
45
 
@@ -65,7 +65,7 @@ export async function createOrUpdateGithubRelease(
65
65
  export function nxProjectChangelogArgs(pkg: ReleasePackageInfo, previousTag: string | null, dryRun: boolean) {
66
66
  const base = {
67
67
  version: pkg.version,
68
- projects: [pkg.name],
68
+ projects: [pkg.projectName],
69
69
  gitCommit: false,
70
70
  gitTag: false,
71
71
  gitPush: false,
@@ -112,7 +112,7 @@ function isPrereleaseVersion(version: string): boolean {
112
112
  return version.includes('-');
113
113
  }
114
114
 
115
- async function withNxWorkspaceRoot<T>(root: string, run: () => Promise<T>): Promise<T> {
115
+ export async function withNxWorkspaceRoot<T>(root: string, run: () => Promise<T>): Promise<T> {
116
116
  const workspaceRootModule = await import('nx/src/utils/workspace-root.js');
117
117
  const previousWorkspaceRoot = workspaceRootModule.workspaceRoot;
118
118
  const previousEnvWorkspaceRoot = process.env.NX_WORKSPACE_ROOT_PATH;