@remix-run/cli 0.2.0 → 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.
- package/dist/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +26 -4
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/completion.js +1 -5
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +3 -3
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +39 -4
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/completion.ts +1 -5
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/{bootstrap → template}/.agents/skills/remix/SKILL.md +160 -73
- package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md +22 -14
- package/{bootstrap → template}/.agents/skills/remix/references/auth-and-sessions.md +41 -18
- package/{bootstrap → template}/.agents/skills/remix/references/data-and-validation.md +21 -5
- package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +43 -53
- package/{bootstrap → template}/.agents/skills/remix/references/routing-and-controllers.md +111 -44
- package/{bootstrap → template}/.agents/skills/remix/references/testing-patterns.md +17 -1
- package/{bootstrap → template}/AGENTS.md +10 -9
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/{bootstrap/app/ui → template/app/assets}/prompt-button.tsx +2 -1
- package/{bootstrap → template}/app/assets.ts +5 -3
- package/template/app/middleware/render.tsx +43 -0
- package/template/app/router.ts +20 -0
- package/{bootstrap → template}/app/routes.ts +1 -2
- package/{bootstrap → template}/app/ui/document.tsx +11 -4
- package/{bootstrap → template}/app/ui/scaffold-home-page.tsx +39 -38
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/{bootstrap → template}/server.ts +10 -11
- package/{bootstrap → template}/tsconfig.json +3 -2
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/assets/entry.ts +0 -19
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -11
- package/bootstrap/app/router.ts +0 -16
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -26
- /package/{bootstrap → template}/.agents/skills/remix/references/animate-elements.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/component-model.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/create-mixins.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/hydration-frames-navigation.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/mixins-styling-events.md +0 -0
|
@@ -12,11 +12,10 @@ import {
|
|
|
12
12
|
} from './errors.ts'
|
|
13
13
|
import { runProgressStep, type StepProgressReporter } from './reporter.ts'
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
19
|
-
const BOOTSTRAP_EXCLUDED_NAMES = new Set(['.gitkeep', 'node_modules'])
|
|
15
|
+
const MODULE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const PACKAGE_TEMPLATE_DIRECTORY = path.resolve(MODULE_DIRECTORY, '../../template')
|
|
17
|
+
const REPO_TEMPLATE_DIRECTORY = path.resolve(MODULE_DIRECTORY, '../../../../template')
|
|
18
|
+
const TEMPLATE_EXCLUDED_NAMES = new Set(['.gitkeep', 'node_modules'])
|
|
20
19
|
export const MINIMUM_SUPPORTED_NODE_VERSION = '24.3.0'
|
|
21
20
|
|
|
22
21
|
export interface BootstrapProjectOptions {
|
|
@@ -67,9 +66,9 @@ export async function bootstrapProject(
|
|
|
67
66
|
await runProgressStep(progress, 'prepare-target-directory', () =>
|
|
68
67
|
ensureTargetDirectory(targetDir, options.force),
|
|
69
68
|
)
|
|
70
|
-
await runProgressStep(progress, 'generate-scaffold-files', () =>
|
|
71
|
-
|
|
72
|
-
sourceDir:
|
|
69
|
+
await runProgressStep(progress, 'generate-scaffold-files', async () =>
|
|
70
|
+
copyTemplateDirectory({
|
|
71
|
+
sourceDir: await resolveTemplateDirectory(),
|
|
73
72
|
targetDir,
|
|
74
73
|
templateValues: createTemplateValues(config),
|
|
75
74
|
}),
|
|
@@ -105,6 +104,34 @@ function createTemplateValues(config: BootstrapConfig): TemplateValues {
|
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
106
|
|
|
107
|
+
async function resolveTemplateDirectory(): Promise<string> {
|
|
108
|
+
if (await isDirectory(PACKAGE_TEMPLATE_DIRECTORY)) {
|
|
109
|
+
return PACKAGE_TEMPLATE_DIRECTORY
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (await isDirectory(REPO_TEMPLATE_DIRECTORY)) {
|
|
113
|
+
return REPO_TEMPLATE_DIRECTORY
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Could not find the Remix app template at ${PACKAGE_TEMPLATE_DIRECTORY} or ${REPO_TEMPLATE_DIRECTORY}.`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function isDirectory(directory: string): Promise<boolean> {
|
|
122
|
+
try {
|
|
123
|
+
let stats = await fs.stat(directory)
|
|
124
|
+
return stats.isDirectory()
|
|
125
|
+
} catch (error) {
|
|
126
|
+
let nodeError = error as NodeJS.ErrnoException
|
|
127
|
+
if (nodeError.code === 'ENOENT') {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
async function writeScaffoldPackageJson(targetDir: string, config: BootstrapConfig): Promise<void> {
|
|
109
136
|
let packageJsonPath = path.join(targetDir, 'package.json')
|
|
110
137
|
let packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as {
|
|
@@ -118,7 +145,6 @@ async function writeScaffoldPackageJson(targetDir: string, config: BootstrapConf
|
|
|
118
145
|
packageJson.dependencies = {
|
|
119
146
|
...packageJson.dependencies,
|
|
120
147
|
remix: config.remixVersion,
|
|
121
|
-
tsx: 'latest',
|
|
122
148
|
}
|
|
123
149
|
packageJson.devDependencies = {
|
|
124
150
|
...packageJson.devDependencies,
|
|
@@ -154,7 +180,7 @@ async function ensureTargetDirectory(targetDir: string, force: boolean): Promise
|
|
|
154
180
|
await fs.mkdir(targetDir, { recursive: true })
|
|
155
181
|
}
|
|
156
182
|
|
|
157
|
-
async function
|
|
183
|
+
async function copyTemplateDirectory({
|
|
158
184
|
sourceDir,
|
|
159
185
|
targetDir,
|
|
160
186
|
templateValues,
|
|
@@ -167,15 +193,15 @@ async function copyBootstrapDirectory({
|
|
|
167
193
|
|
|
168
194
|
for (let entry of entries) {
|
|
169
195
|
let sourcePath = path.join(sourceDir, entry.name)
|
|
170
|
-
let targetPath = path.join(targetDir, entry.name)
|
|
196
|
+
let targetPath = path.join(targetDir, entry.name === 'gitignore' ? '.gitignore' : entry.name)
|
|
171
197
|
|
|
172
|
-
if (
|
|
198
|
+
if (TEMPLATE_EXCLUDED_NAMES.has(entry.name)) {
|
|
173
199
|
continue
|
|
174
200
|
}
|
|
175
201
|
|
|
176
202
|
if (entry.isDirectory()) {
|
|
177
203
|
await fs.mkdir(targetPath, { recursive: true })
|
|
178
|
-
await
|
|
204
|
+
await copyTemplateDirectory({ sourceDir: sourcePath, targetDir: targetPath, templateValues })
|
|
179
205
|
continue
|
|
180
206
|
}
|
|
181
207
|
|
package/src/lib/cli.ts
CHANGED
|
@@ -5,11 +5,40 @@ import { resolveCliContext, type CliContext } from './cli-context.ts'
|
|
|
5
5
|
import { renderCliError, unknownCommand } from './errors.ts'
|
|
6
6
|
import { configureColors, restoreTerminalFormatting } from './terminal.ts'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Options accepted by {@link runRemix}.
|
|
10
|
+
*/
|
|
8
11
|
export interface RunRemixOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Directory used as the working root when resolving project files (default
|
|
14
|
+
* `process.cwd()`).
|
|
15
|
+
*/
|
|
9
16
|
cwd?: string
|
|
17
|
+
/**
|
|
18
|
+
* Override for the Remix version reported by `remix --version`. Defaults to
|
|
19
|
+
* the version of the installed `@remix-run/cli` package.
|
|
20
|
+
*/
|
|
10
21
|
remixVersion?: string
|
|
11
22
|
}
|
|
12
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Entry point for the `remix` CLI. Parses `argv`, dispatches to the matching
|
|
26
|
+
* subcommand (`new`, `doctor`, `routes`, `test`, `version`, `completion`,
|
|
27
|
+
* `help`), and resolves with the exit code the process should use.
|
|
28
|
+
*
|
|
29
|
+
* @param argv Argument vector to parse, excluding the node and script paths
|
|
30
|
+
* (default `process.argv.slice(2)`).
|
|
31
|
+
* @param options Overrides for CLI context resolution.
|
|
32
|
+
* @returns The numeric exit code (`0` on success, non-zero on failure).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { runRemix } from '@remix-run/cli'
|
|
37
|
+
*
|
|
38
|
+
* let exitCode = await runRemix(process.argv.slice(2))
|
|
39
|
+
* process.exit(exitCode)
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
13
42
|
export async function runRemix(
|
|
14
43
|
argv: string[] = process.argv.slice(2),
|
|
15
44
|
options: RunRemixOptions = {},
|
|
@@ -17,12 +46,10 @@ export async function runRemix(
|
|
|
17
46
|
let context = await resolveCliContext(options)
|
|
18
47
|
|
|
19
48
|
try {
|
|
20
|
-
|
|
21
|
-
argv = argv.slice(1)
|
|
22
|
-
}
|
|
49
|
+
argv = stripLeadingSeparators(argv)
|
|
23
50
|
|
|
24
51
|
let globalOptions = extractGlobalOptions(argv)
|
|
25
|
-
argv = globalOptions.argv
|
|
52
|
+
argv = stripLeadingSeparators(globalOptions.argv)
|
|
26
53
|
configureColors({ disabled: globalOptions.noColor })
|
|
27
54
|
|
|
28
55
|
if (argv.length === 0) {
|
|
@@ -43,6 +70,14 @@ export async function runRemix(
|
|
|
43
70
|
}
|
|
44
71
|
}
|
|
45
72
|
|
|
73
|
+
function stripLeadingSeparators(argv: string[]): string[] {
|
|
74
|
+
while (argv[0] === '--') {
|
|
75
|
+
argv = argv.slice(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return argv
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
async function runCommand(command: string, argv: string[], context: CliContext): Promise<number> {
|
|
47
82
|
if (command === 'help') {
|
|
48
83
|
return runHelpCommand(argv)
|
|
@@ -16,7 +16,7 @@ import { formatHelpText } from '../help-text.ts'
|
|
|
16
16
|
import { parseArgs } from '../parse-args.ts'
|
|
17
17
|
|
|
18
18
|
export async function runCompletionCommand(argv: string[]): Promise<number> {
|
|
19
|
-
if (argv.length === 0
|
|
19
|
+
if (argv.length === 0) {
|
|
20
20
|
process.stdout.write(getCompletionCommandHelpText())
|
|
21
21
|
return 0
|
|
22
22
|
}
|
|
@@ -25,6 +25,11 @@ export async function runCompletionCommand(argv: string[]): Promise<number> {
|
|
|
25
25
|
return runCompletionPlumbing(argv.slice(1))
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
29
|
+
process.stdout.write(getCompletionCommandHelpText())
|
|
30
|
+
return 0
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
let [shell, ...rest] = argv
|
|
29
34
|
|
|
30
35
|
try {
|
|
@@ -26,9 +26,9 @@ import {
|
|
|
26
26
|
} from '../reporter.ts'
|
|
27
27
|
|
|
28
28
|
const DOCTOR_SUITE_LABELS = {
|
|
29
|
-
|
|
30
|
-
complete: '
|
|
31
|
-
running: 'Checking
|
|
29
|
+
actions: {
|
|
30
|
+
complete: 'actions',
|
|
31
|
+
running: 'Checking actions',
|
|
32
32
|
},
|
|
33
33
|
environment: {
|
|
34
34
|
complete: 'environment',
|
|
@@ -109,7 +109,7 @@ export function getDoctorCommandHelpText(target: NodeJS.WriteStream = process.st
|
|
|
109
109
|
description: 'Exit with status 1 when warning-level findings are present',
|
|
110
110
|
label: '--strict',
|
|
111
111
|
},
|
|
112
|
-
{ description: 'Apply low-risk project and
|
|
112
|
+
{ description: 'Apply low-risk project and action fixes', label: '--fix' },
|
|
113
113
|
],
|
|
114
114
|
usage: ['remix doctor [--json] [--strict] [--fix] [--no-color]'],
|
|
115
115
|
},
|
|
@@ -182,16 +182,13 @@ async function collectDoctorReport(
|
|
|
182
182
|
|
|
183
183
|
if (hasWarningFindings(environment.suite.findings)) {
|
|
184
184
|
let projectSuite = createSkippedDoctorSuite('project', 'Blocked by environment warnings.')
|
|
185
|
-
let
|
|
186
|
-
|
|
187
|
-
'Blocked by environment warnings.',
|
|
188
|
-
)
|
|
189
|
-
suites.push(projectSuite, controllersSuite)
|
|
185
|
+
let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by environment warnings.')
|
|
186
|
+
suites.push(projectSuite, actionsSuite)
|
|
190
187
|
progress?.skip(projectSuite.name, projectSuite.reason)
|
|
191
188
|
writeDoctorSuiteDetails(reporter, projectSuite)
|
|
192
189
|
progress?.writeSummaryGap()
|
|
193
|
-
progress?.skip(
|
|
194
|
-
writeDoctorSuiteDetails(reporter,
|
|
190
|
+
progress?.skip(actionsSuite.name, actionsSuite.reason)
|
|
191
|
+
writeDoctorSuiteDetails(reporter, actionsSuite)
|
|
195
192
|
progress?.writeSummaryGap()
|
|
196
193
|
|
|
197
194
|
return {
|
|
@@ -240,10 +237,10 @@ async function collectDoctorReport(
|
|
|
240
237
|
progress?.writeSummaryGap()
|
|
241
238
|
|
|
242
239
|
if (hasWarningFindings(project.suite.findings)) {
|
|
243
|
-
let
|
|
244
|
-
suites.push(
|
|
245
|
-
progress?.skip(
|
|
246
|
-
writeDoctorSuiteDetails(reporter,
|
|
240
|
+
let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by project warnings.')
|
|
241
|
+
suites.push(actionsSuite)
|
|
242
|
+
progress?.skip(actionsSuite.name, actionsSuite.reason)
|
|
243
|
+
writeDoctorSuiteDetails(reporter, actionsSuite)
|
|
247
244
|
progress?.writeSummaryGap()
|
|
248
245
|
|
|
249
246
|
return {
|
|
@@ -256,7 +253,7 @@ async function collectDoctorReport(
|
|
|
256
253
|
}
|
|
257
254
|
}
|
|
258
255
|
|
|
259
|
-
let
|
|
256
|
+
let actions = await runDoctorSuite(progress, 'actions', async () => {
|
|
260
257
|
let controllerResult = await checkControllerConventions(
|
|
261
258
|
project.routeManifest!.appRoot,
|
|
262
259
|
project.routeManifest!.tree,
|
|
@@ -276,7 +273,7 @@ async function collectDoctorReport(
|
|
|
276
273
|
remainingFindings = finalControllerResult.suite.findings
|
|
277
274
|
}
|
|
278
275
|
|
|
279
|
-
let suite = createDoctorSuite('
|
|
276
|
+
let suite = createDoctorSuite('actions', remainingFindings)
|
|
280
277
|
if (suiteAppliedFixes.length > 0) {
|
|
281
278
|
suite.appliedFixes = suiteAppliedFixes
|
|
282
279
|
}
|
|
@@ -284,10 +281,10 @@ async function collectDoctorReport(
|
|
|
284
281
|
return { appliedFixes: suiteAppliedFixes, suite }
|
|
285
282
|
})
|
|
286
283
|
|
|
287
|
-
findings.push(...
|
|
288
|
-
appliedFixes.push(...(
|
|
289
|
-
suites.push(
|
|
290
|
-
writeDoctorSuiteDetails(reporter,
|
|
284
|
+
findings.push(...actions.suite.findings)
|
|
285
|
+
appliedFixes.push(...(actions.appliedFixes ?? []))
|
|
286
|
+
suites.push(actions.suite)
|
|
287
|
+
writeDoctorSuiteDetails(reporter, actions.suite)
|
|
291
288
|
progress?.writeSummaryGap()
|
|
292
289
|
|
|
293
290
|
let report: DoctorReport = {
|
|
@@ -8,7 +8,7 @@ import { createCommandReporter, type TextChannel } from '../reporter.ts'
|
|
|
8
8
|
import { loadRouteMap, type LoadedRouteMap, type RouteTreeNode } from '../route-map.ts'
|
|
9
9
|
import { lightRed } from '../terminal.ts'
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const ACTIONS_PATH_PREFIX = 'app/actions/'
|
|
12
12
|
|
|
13
13
|
export async function runRoutesCommand(argv: string[], context: CliContext): Promise<number> {
|
|
14
14
|
if (argv.includes('-h') || argv.includes('--help')) {
|
|
@@ -243,8 +243,8 @@ function colorRouteLine(line: string, node: RouteTreeNode): string {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
function getCompactOwnerPath(ownerPath: string): string {
|
|
246
|
-
if (ownerPath.startsWith(
|
|
247
|
-
return ownerPath.slice(
|
|
246
|
+
if (ownerPath.startsWith(ACTIONS_PATH_PREFIX)) {
|
|
247
|
+
return ownerPath.slice(ACTIONS_PATH_PREFIX.length)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
return ownerPath
|
package/src/lib/completion.ts
CHANGED
|
@@ -169,11 +169,7 @@ function completeCommand(
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
if (command === 'test') {
|
|
172
|
-
return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [
|
|
173
|
-
'--coverage',
|
|
174
|
-
'--watch',
|
|
175
|
-
'--help',
|
|
176
|
-
])
|
|
172
|
+
return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, ['--coverage', '--watch'])
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
if (command === 'completion') {
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
2
|
|
|
3
3
|
const OWNER_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const
|
|
4
|
-
|
|
4
|
+
const ACTIONS_DIRECTORY = path.join('app', 'actions')
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
return OWNER_FILE_EXTENSIONS.map((extension) =>
|
|
8
|
-
normalizeRelativePath(path.join('app', 'controllers', `${path.join(...segments)}${extension}`)),
|
|
9
|
-
)
|
|
10
|
-
}
|
|
6
|
+
export type OwnerFileExtension = (typeof OWNER_FILE_EXTENSIONS)[number]
|
|
11
7
|
|
|
12
8
|
export function getControllerOwnerCandidates(segments: string[]): string[] {
|
|
13
9
|
return OWNER_FILE_EXTENSIONS.map((extension) =>
|
|
14
|
-
normalizeRelativePath(path.join(
|
|
10
|
+
normalizeRelativePath(path.join(ACTIONS_DIRECTORY, ...segments, `controller${extension}`)),
|
|
15
11
|
)
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export function getRouteSubtreePath(segments: string[]): string {
|
|
19
|
-
return normalizeRelativePath(path.join(
|
|
15
|
+
return normalizeRelativePath(path.join(ACTIONS_DIRECTORY, ...segments))
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export function getPreferredOwnerDisplayPath(candidates: string[]): string {
|