sdnext 0.0.26 → 0.0.27

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.
@@ -1,15 +1,19 @@
1
1
  import { join } from "path";
2
+ import { resolveProjectImportPath } from "./resolveProjectImportPath.js";
2
3
  import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact.js";
3
4
  async function createAction(path) {
4
5
  const info = getSharedModuleInfo(path);
5
6
  if (!isScriptModule(info.relativePath)) return;
7
+ const actionPath = join("actions", info.relativePath);
8
+ const createResponseFnImportPath = await resolveProjectImportPath(actionPath, "server/createResponseFn");
9
+ const sharedImportPath = await resolveProjectImportPath(actionPath, `shared/${info.importPath}`);
6
10
  await writeGeneratedFile({
7
- path: join("actions", info.relativePath),
11
+ path: actionPath,
8
12
  content: `"use server"
9
13
 
10
- import { createResponseFn } from "@/server/createResponseFn"
14
+ import { createResponseFn } from "${createResponseFnImportPath}"
11
15
 
12
- import { ${info.name} } from "@/shared/${info.importPath}"
16
+ import { ${info.name} } from "${sharedImportPath}"
13
17
 
14
18
  export const ${info.name}Action = createResponseFn(${info.name})
15
19
  `
@@ -1,5 +1,6 @@
1
1
  import { readdir } from "fs/promises";
2
2
  import { join } from "path";
3
+ import { resolveProjectImportPath } from "./resolveProjectImportPath.js";
3
4
  import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact.js";
4
5
  async function createRoute(path) {
5
6
  if (path) {
@@ -7,9 +8,10 @@ async function createRoute(path) {
7
8
  if (!isScriptModule(info.relativePath)) return;
8
9
  }
9
10
  const modules = await getSharedModules("shared");
11
+ const routePath = join("app", "api", "action", "[...action]", "route.ts");
10
12
  await writeGeneratedFile({
11
- path: join("app", "api", "action", "[...action]", "route.ts"),
12
- content: getRouteFileContent(modules)
13
+ path: routePath,
14
+ content: await getRouteFileContent(modules, routePath)
13
15
  });
14
16
  }
15
17
  async function getSharedModules(dir) {
@@ -33,12 +35,16 @@ async function getSharedModules(dir) {
33
35
  modules.sort((a, b)=>a.importPath.localeCompare(b.importPath));
34
36
  return modules;
35
37
  }
36
- function getRouteFileContent(items) {
37
- const importLines = items.map((item)=>`import { ${item.name} } from "@/shared/${item.importPath}"`).join("\n");
38
+ async function getRouteFileContent(items, routePath) {
39
+ const createRouteFnImportPath = await resolveProjectImportPath(routePath, "server/createResponseFn");
40
+ const importLines = (await Promise.all(items.map(async (item)=>{
41
+ const importPath = await resolveProjectImportPath(routePath, `shared/${item.importPath}`);
42
+ return `import { ${item.name} } from "${importPath}"`;
43
+ }))).join("\n");
38
44
  const registerLines = items.map((item)=>`registerRoute(${item.name})`).join("\n");
39
45
  return `import { NextRequest, NextResponse } from "next/server"
40
46
 
41
- import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "@/server/createResponseFn"
47
+ import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "${createRouteFnImportPath}"
42
48
  ${importLines ? `\n${importLines}\n` : ""}
43
49
  const routeMap = new Map<string, RouteHandler>()
44
50
 
@@ -3,6 +3,7 @@ import { join, parse, relative } from "path";
3
3
  import { cwd } from "process";
4
4
  import { checkbox as prompts_checkbox, select as prompts_select } from "@inquirer/prompts";
5
5
  import { readSdNextSetting } from "./readSdNextSetting.js";
6
+ import { resolveProjectImportPath } from "./resolveProjectImportPath.js";
6
7
  import { isScriptModule, normalizePathSeparator, writeGeneratedFile } from "./sharedArtifact.js";
7
8
  import { writeSdNextSetting } from "./writeSdNextSetting.js";
8
9
  function getHookTypeFromName(name) {
@@ -20,7 +21,7 @@ async function getHookTypeFromContent(path, content) {
20
21
  const type = setting.hook?.[path];
21
22
  if (void 0 !== type && "skip" !== type) return type;
22
23
  if (content.includes("useMutation")) return "mutation";
23
- if (content.includes("createUse") && content.includes("@/presets/")) return "mutation";
24
+ if (content.includes("createUse") && /from\s+["'][^"']*\/presets\//.test(content)) return "mutation";
24
25
  if (content.includes("ClientOptional")) return "get";
25
26
  if (content.includes("useQuery")) return "query";
26
27
  }
@@ -51,11 +52,15 @@ async function createHook(path, hookMap) {
51
52
  const mutationPresetPath = join("presets", dir, mutationPresetName);
52
53
  const mutationPresetImportPath = normalizePathSeparator(join(dir, `createUse${upName}`));
53
54
  const clientInputType = `${upName}ClientInput`;
55
+ const actionPath = normalizePathSeparator(join("actions", actionImportPath));
56
+ const hookActionImportPath = await resolveProjectImportPath(hookPath, actionPath);
57
+ const hookPresetImportPath = await resolveProjectImportPath(hookPath, normalizePathSeparator(join("presets", mutationPresetImportPath)));
58
+ const mutationPresetSharedImportPath = await resolveProjectImportPath(mutationPresetPath, normalizePathSeparator(join("shared", actionImportPath)));
54
59
  const mutationHook = `import { createRequestFn } from "deepsea-tools"
55
60
 
56
- import { ${name}Action } from "@/actions/${actionImportPath}"
61
+ import { ${name}Action } from "${hookActionImportPath}"
57
62
 
58
- import { createUse${upName} } from "@/presets/${mutationPresetImportPath}"
63
+ import { createUse${upName} } from "${hookPresetImportPath}"
59
64
 
60
65
  export const ${name}Client = createRequestFn(${name}Action)
61
66
 
@@ -65,7 +70,7 @@ export const use${upName} = createUse${upName}(${name}Client)
65
70
 
66
71
  import { withUseMutationDefaults } from "soda-tanstack-query"
67
72
 
68
- import { ${name} } from "@/shared/${actionImportPath}"
73
+ import { ${name} } from "${mutationPresetSharedImportPath}"
69
74
 
70
75
  export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() => {
71
76
  const key = useId()
@@ -99,7 +104,7 @@ export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() =>
99
104
  const getHook = `import { createRequestFn, isNonNullable } from "deepsea-tools"
100
105
  import { createUseQuery } from "soda-tanstack-query"
101
106
 
102
- import { ${name}Action } from "@/actions/${actionImportPath}"
107
+ import { ${name}Action } from "${hookActionImportPath}"
103
108
 
104
109
  export const ${name}Client = createRequestFn(${name}Action)
105
110
 
@@ -117,7 +122,7 @@ export const use${upName} = createUseQuery({
117
122
  const queryHook = `import { createRequestFn } from "deepsea-tools"
118
123
  import { createUseQuery } from "soda-tanstack-query"
119
124
 
120
- import { ${name}Action } from "@/actions/${actionImportPath}"
125
+ import { ${name}Action } from "${hookActionImportPath}"
121
126
 
122
127
  export const ${name}Client = createRequestFn(${name}Action)
123
128
 
@@ -0,0 +1 @@
1
+ export declare function resolveProjectImportPath(fromPath: string, targetPath: string): Promise<string>;
@@ -0,0 +1,201 @@
1
+ import { constants } from "node:fs";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
5
+ import { cwd } from "node:process";
6
+ import { normalizePathSeparator } from "./sharedArtifact.js";
7
+ const resolveProjectImportPath_require = createRequire(import.meta.url);
8
+ const projectRoot = cwd();
9
+ let rootAliasPromise;
10
+ async function resolveProjectImportPath(fromPath, targetPath) {
11
+ const normalizedTargetPath = normalizePathSeparator(targetPath).replace(/^\.?\//, "");
12
+ const rootAlias = await getProjectRootAlias();
13
+ if (rootAlias) return `${rootAlias}/${normalizedTargetPath}`;
14
+ return getRelativeImportPath(fromPath, normalizedTargetPath);
15
+ }
16
+ function getRelativeImportPath(fromPath, targetPath) {
17
+ const fromDir = dirname(fromPath);
18
+ let importPath = normalizePathSeparator(relative(fromDir, targetPath));
19
+ if (!importPath.startsWith(".")) importPath = `./${importPath}`;
20
+ return importPath;
21
+ }
22
+ async function getProjectRootAlias() {
23
+ rootAliasPromise ??= resolveProjectRootAlias();
24
+ return rootAliasPromise;
25
+ }
26
+ async function resolveProjectRootAlias() {
27
+ const configPath = await findProjectConfigPath(projectRoot);
28
+ if (!configPath) return;
29
+ return readRootAliasFromConfig(configPath, new Set());
30
+ }
31
+ async function findProjectConfigPath(path) {
32
+ const candidates = [
33
+ join(path, "tsconfig.json"),
34
+ join(path, "jsconfig.json")
35
+ ];
36
+ for (const candidate of candidates)if (await exists(candidate)) return candidate;
37
+ }
38
+ async function readRootAliasFromConfig(configPath, seen) {
39
+ const normalizedPath = normalize(configPath);
40
+ if (seen.has(normalizedPath)) return;
41
+ seen.add(normalizedPath);
42
+ const config = await readJsonConfig(configPath);
43
+ const compilerOptions = toObject(config.compilerOptions);
44
+ const configDir = dirname(configPath);
45
+ const baseDir = getBaseDir(configDir, compilerOptions.baseUrl);
46
+ const paths = toPathMap(compilerOptions.paths);
47
+ if (paths) {
48
+ const rootAlias = findRootAlias(paths, baseDir, projectRoot);
49
+ if (rootAlias) return rootAlias;
50
+ }
51
+ const extendsValue = "string" == typeof config.extends ? config.extends : void 0;
52
+ if (!extendsValue) return;
53
+ const extendsPath = await resolveExtendsPath(extendsValue, configDir);
54
+ if (!extendsPath) return;
55
+ return readRootAliasFromConfig(extendsPath, seen);
56
+ }
57
+ function getBaseDir(configDir, baseUrl) {
58
+ if ("string" != typeof baseUrl) return configDir;
59
+ return resolve(configDir, baseUrl);
60
+ }
61
+ function findRootAlias(paths, baseDir, rootDir) {
62
+ for (const [key, targets] of Object.entries(paths)){
63
+ const alias = getAliasName(key);
64
+ if (alias) {
65
+ for (const target of targets)if (isRootTarget(target, baseDir, rootDir)) return alias;
66
+ }
67
+ }
68
+ }
69
+ function getAliasName(pattern) {
70
+ const wildcard = splitWildcard(pattern);
71
+ if (!wildcard || wildcard.suffix.length > 0) return;
72
+ const aliasPrefix = wildcard.prefix.replace(/\/$/, "");
73
+ if (!aliasPrefix) return;
74
+ return aliasPrefix;
75
+ }
76
+ function isRootTarget(pattern, baseDir, rootDir) {
77
+ const wildcard = splitWildcard(pattern);
78
+ if (!wildcard || wildcard.suffix.length > 0) return false;
79
+ const targetPath = resolve(baseDir, wildcard.prefix || ".");
80
+ return normalizeForComparison(targetPath) === normalizeForComparison(rootDir);
81
+ }
82
+ function splitWildcard(value) {
83
+ const index = value.indexOf("*");
84
+ if (index < 0) return;
85
+ if (value.indexOf("*", index + 1) >= 0) return;
86
+ return {
87
+ prefix: value.slice(0, index),
88
+ suffix: value.slice(index + 1)
89
+ };
90
+ }
91
+ async function resolveExtendsPath(extendsValue, baseDir) {
92
+ if (isAbsolute(extendsValue) || extendsValue.startsWith(".")) {
93
+ const relativeCandidates = getRelativeCandidates(resolve(baseDir, extendsValue));
94
+ for (const candidate of relativeCandidates)if (await exists(candidate)) return candidate;
95
+ return;
96
+ }
97
+ const moduleCandidates = getModuleCandidates(extendsValue);
98
+ for (const candidate of moduleCandidates)try {
99
+ return resolveProjectImportPath_require.resolve(candidate, {
100
+ paths: [
101
+ baseDir
102
+ ]
103
+ });
104
+ } catch (error) {}
105
+ }
106
+ function getRelativeCandidates(path) {
107
+ if (path.endsWith(".json")) return [
108
+ path
109
+ ];
110
+ return [
111
+ path,
112
+ `${path}.json`
113
+ ];
114
+ }
115
+ function getModuleCandidates(path) {
116
+ if (path.endsWith(".json")) return [
117
+ path
118
+ ];
119
+ return [
120
+ path,
121
+ `${path}.json`
122
+ ];
123
+ }
124
+ async function readJsonConfig(path) {
125
+ const content = await readFile(path, "utf-8");
126
+ const parsed = JSON.parse(stripTrailingCommas(stripComments(content)));
127
+ return toObject(parsed);
128
+ }
129
+ function stripComments(content) {
130
+ const output = [];
131
+ let inString = false;
132
+ let escaped = false;
133
+ for(let index = 0; index < content.length; index++){
134
+ const char = content[index];
135
+ const next = content[index + 1];
136
+ if (inString) {
137
+ output.push(char);
138
+ if (escaped) {
139
+ escaped = false;
140
+ continue;
141
+ }
142
+ if ("\\" === char) {
143
+ escaped = true;
144
+ continue;
145
+ }
146
+ if ("\"" === char) inString = false;
147
+ continue;
148
+ }
149
+ if ("\"" === char) {
150
+ inString = true;
151
+ output.push(char);
152
+ continue;
153
+ }
154
+ if ("/" === char && "/" === next) {
155
+ while(index < content.length && "\n" !== content[index])index++;
156
+ output.push("\n");
157
+ continue;
158
+ }
159
+ if ("/" === char && "*" === next) {
160
+ index += 2;
161
+ while(index < content.length && !("*" === content[index] && "/" === content[index + 1]))index++;
162
+ index++;
163
+ continue;
164
+ }
165
+ output.push(char);
166
+ }
167
+ return output.join("").replace(/^\uFEFF/, "");
168
+ }
169
+ function stripTrailingCommas(content) {
170
+ return content.replace(/,\s*([}\]])/g, "$1");
171
+ }
172
+ function toPathMap(value) {
173
+ if (!value || "object" != typeof value || Array.isArray(value)) return;
174
+ const entries = [];
175
+ for (const [key, item] of Object.entries(value)){
176
+ if (!Array.isArray(item)) continue;
177
+ const list = item.filter((target)=>"string" == typeof target);
178
+ if (0 !== list.length) entries.push([
179
+ key,
180
+ list
181
+ ]);
182
+ }
183
+ if (0 === entries.length) return;
184
+ return Object.fromEntries(entries);
185
+ }
186
+ function toObject(value) {
187
+ if (value && "object" == typeof value && !Array.isArray(value)) return value;
188
+ return {};
189
+ }
190
+ async function exists(path) {
191
+ try {
192
+ await access(path, constants.F_OK);
193
+ return true;
194
+ } catch (error) {
195
+ return false;
196
+ }
197
+ }
198
+ function normalizeForComparison(path) {
199
+ return normalize(path).replace(/\\/g, "/").replace(/\/$/, "").toLowerCase();
200
+ }
201
+ export { resolveProjectImportPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdnext",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,6 @@
1
1
  import { join } from "path"
2
2
 
3
+ import { resolveProjectImportPath } from "./resolveProjectImportPath"
3
4
  import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact"
4
5
 
5
6
  export async function createAction(path: string) {
@@ -7,13 +8,17 @@ export async function createAction(path: string) {
7
8
 
8
9
  if (!isScriptModule(info.relativePath)) return
9
10
 
11
+ const actionPath = join("actions", info.relativePath)
12
+ const createResponseFnImportPath = await resolveProjectImportPath(actionPath, "server/createResponseFn")
13
+ const sharedImportPath = await resolveProjectImportPath(actionPath, `shared/${info.importPath}`)
14
+
10
15
  await writeGeneratedFile({
11
- path: join("actions", info.relativePath),
16
+ path: actionPath,
12
17
  content: `"use server"
13
18
 
14
- import { createResponseFn } from "@/server/createResponseFn"
19
+ import { createResponseFn } from "${createResponseFnImportPath}"
15
20
 
16
- import { ${info.name} } from "@/shared/${info.importPath}"
21
+ import { ${info.name} } from "${sharedImportPath}"
17
22
 
18
23
  export const ${info.name}Action = createResponseFn(${info.name})
19
24
  `
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "fs/promises"
2
2
  import { join } from "path"
3
3
 
4
+ import { resolveProjectImportPath } from "./resolveProjectImportPath"
4
5
  import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact"
5
6
 
6
7
  export async function createRoute(path?: string) {
@@ -10,10 +11,11 @@ export async function createRoute(path?: string) {
10
11
  }
11
12
 
12
13
  const modules = await getSharedModules("shared")
14
+ const routePath = join("app", "api", "action", "[...action]", "route.ts")
13
15
 
14
16
  await writeGeneratedFile({
15
- path: join("app", "api", "action", "[...action]", "route.ts"),
16
- content: getRouteFileContent(modules),
17
+ path: routePath,
18
+ content: await getRouteFileContent(modules, routePath),
17
19
  })
18
20
  }
19
21
 
@@ -55,13 +57,17 @@ export interface GetRouteFileContentParamsItem {
55
57
  name: string
56
58
  }
57
59
 
58
- function getRouteFileContent(items: GetRouteFileContentParamsItem[]) {
59
- const importLines = items.map(item => `import { ${item.name} } from "@/shared/${item.importPath}"`).join("\n")
60
+ async function getRouteFileContent(items: GetRouteFileContentParamsItem[], routePath: string) {
61
+ const createRouteFnImportPath = await resolveProjectImportPath(routePath, "server/createResponseFn")
62
+ const importLines = (await Promise.all(items.map(async (item) => {
63
+ const importPath = await resolveProjectImportPath(routePath, `shared/${item.importPath}`)
64
+ return `import { ${item.name} } from "${importPath}"`
65
+ }))).join("\n")
60
66
  const registerLines = items.map(item => `registerRoute(${item.name})`).join("\n")
61
67
 
62
68
  return `import { NextRequest, NextResponse } from "next/server"
63
69
 
64
- import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "@/server/createResponseFn"
70
+ import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "${createRouteFnImportPath}"
65
71
  ${importLines ? `\n${importLines}\n` : ""}
66
72
  const routeMap = new Map<string, RouteHandler>()
67
73
 
package/src/utils/hook.ts CHANGED
@@ -6,6 +6,7 @@ import { checkbox, select } from "@inquirer/prompts"
6
6
  import { Command } from "commander"
7
7
 
8
8
  import { readSdNextSetting, SdNextSetting } from "./readSdNextSetting"
9
+ import { resolveProjectImportPath } from "./resolveProjectImportPath"
9
10
  import { isScriptModule, normalizePathSeparator, writeGeneratedFile } from "./sharedArtifact"
10
11
  import { writeSdNextSetting } from "./writeSdNextSetting"
11
12
 
@@ -33,7 +34,7 @@ async function getHookTypeFromContent(path: string, content: string): Promise<Ho
33
34
  const type = setting.hook?.[path]
34
35
  if (type !== undefined && type !== "skip") return type
35
36
  if (content.includes("useMutation")) return "mutation"
36
- if (content.includes("createUse") && content.includes("@/presets/")) return "mutation"
37
+ if (content.includes("createUse") && /from\s+["'][^"']*\/presets\//.test(content)) return "mutation"
37
38
  if (content.includes("ClientOptional")) return "get"
38
39
  if (content.includes("useQuery")) return "query"
39
40
  return undefined
@@ -81,12 +82,16 @@ export async function createHook(path: string, hookMap: Record<string, HookData>
81
82
  const mutationPresetPath = join("presets", dir, mutationPresetName)
82
83
  const mutationPresetImportPath = normalizePathSeparator(join(dir, `createUse${upName}`))
83
84
  const clientInputType = `${upName}ClientInput`
85
+ const actionPath = normalizePathSeparator(join("actions", actionImportPath))
86
+ const hookActionImportPath = await resolveProjectImportPath(hookPath, actionPath)
87
+ const hookPresetImportPath = await resolveProjectImportPath(hookPath, normalizePathSeparator(join("presets", mutationPresetImportPath)))
88
+ const mutationPresetSharedImportPath = await resolveProjectImportPath(mutationPresetPath, normalizePathSeparator(join("shared", actionImportPath)))
84
89
 
85
90
  const mutationHook = `import { createRequestFn } from "deepsea-tools"
86
91
 
87
- import { ${name}Action } from "@/actions/${actionImportPath}"
92
+ import { ${name}Action } from "${hookActionImportPath}"
88
93
 
89
- import { createUse${upName} } from "@/presets/${mutationPresetImportPath}"
94
+ import { createUse${upName} } from "${hookPresetImportPath}"
90
95
 
91
96
  export const ${name}Client = createRequestFn(${name}Action)
92
97
 
@@ -97,7 +102,7 @@ export const use${upName} = createUse${upName}(${name}Client)
97
102
 
98
103
  import { withUseMutationDefaults } from "soda-tanstack-query"
99
104
 
100
- import { ${name} } from "@/shared/${actionImportPath}"
105
+ import { ${name} } from "${mutationPresetSharedImportPath}"
101
106
 
102
107
  export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() => {
103
108
  const key = useId()
@@ -132,7 +137,7 @@ export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() =>
132
137
  const getHook = `import { createRequestFn, isNonNullable } from "deepsea-tools"
133
138
  import { createUseQuery } from "soda-tanstack-query"
134
139
 
135
- import { ${name}Action } from "@/actions/${actionImportPath}"
140
+ import { ${name}Action } from "${hookActionImportPath}"
136
141
 
137
142
  export const ${name}Client = createRequestFn(${name}Action)
138
143
 
@@ -151,7 +156,7 @@ export const use${upName} = createUseQuery({
151
156
  const queryHook = `import { createRequestFn } from "deepsea-tools"
152
157
  import { createUseQuery } from "soda-tanstack-query"
153
158
 
154
- import { ${name}Action } from "@/actions/${actionImportPath}"
159
+ import { ${name}Action } from "${hookActionImportPath}"
155
160
 
156
161
  export const ${name}Client = createRequestFn(${name}Action)
157
162
 
@@ -0,0 +1,256 @@
1
+ import { constants } from "node:fs"
2
+ import { access, readFile } from "node:fs/promises"
3
+ import { createRequire } from "node:module"
4
+ import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path"
5
+ import { cwd } from "node:process"
6
+
7
+ import { normalizePathSeparator } from "./sharedArtifact"
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ const projectRoot = cwd()
12
+
13
+ let rootAliasPromise: Promise<string | undefined> | undefined
14
+
15
+ export async function resolveProjectImportPath(fromPath: string, targetPath: string) {
16
+ const normalizedTargetPath = normalizePathSeparator(targetPath).replace(/^\.?\//, "")
17
+ const rootAlias = await getProjectRootAlias()
18
+
19
+ if (rootAlias) return `${rootAlias}/${normalizedTargetPath}`
20
+
21
+ return getRelativeImportPath(fromPath, normalizedTargetPath)
22
+ }
23
+
24
+ function getRelativeImportPath(fromPath: string, targetPath: string) {
25
+ const fromDir = dirname(fromPath)
26
+ let importPath = normalizePathSeparator(relative(fromDir, targetPath))
27
+
28
+ if (!importPath.startsWith(".")) importPath = `./${importPath}`
29
+
30
+ return importPath
31
+ }
32
+
33
+ async function getProjectRootAlias() {
34
+ rootAliasPromise ??= resolveProjectRootAlias()
35
+ return rootAliasPromise
36
+ }
37
+
38
+ async function resolveProjectRootAlias() {
39
+ const configPath = await findProjectConfigPath(projectRoot)
40
+ if (!configPath) return undefined
41
+
42
+ return readRootAliasFromConfig(configPath, new Set())
43
+ }
44
+
45
+ async function findProjectConfigPath(path: string) {
46
+ const candidates = [join(path, "tsconfig.json"), join(path, "jsconfig.json")]
47
+
48
+ for (const candidate of candidates) {
49
+ if (await exists(candidate)) return candidate
50
+ }
51
+
52
+ return undefined
53
+ }
54
+
55
+ async function readRootAliasFromConfig(configPath: string, seen: Set<string>): Promise<string | undefined> {
56
+ const normalizedPath = normalize(configPath)
57
+ if (seen.has(normalizedPath)) return undefined
58
+ seen.add(normalizedPath)
59
+
60
+ const config = await readJsonConfig(configPath)
61
+ const compilerOptions = toObject(config.compilerOptions)
62
+ const configDir = dirname(configPath)
63
+ const baseDir = getBaseDir(configDir, compilerOptions.baseUrl)
64
+ const paths = toPathMap(compilerOptions.paths)
65
+
66
+ if (paths) {
67
+ const rootAlias = findRootAlias(paths, baseDir, projectRoot)
68
+ if (rootAlias) return rootAlias
69
+ }
70
+
71
+ const extendsValue = typeof config.extends === "string" ? config.extends : undefined
72
+ if (!extendsValue) return undefined
73
+
74
+ const extendsPath = await resolveExtendsPath(extendsValue, configDir)
75
+ if (!extendsPath) return undefined
76
+
77
+ return readRootAliasFromConfig(extendsPath, seen)
78
+ }
79
+
80
+ function getBaseDir(configDir: string, baseUrl: unknown) {
81
+ if (typeof baseUrl !== "string") return configDir
82
+ return resolve(configDir, baseUrl)
83
+ }
84
+
85
+ function findRootAlias(paths: Record<string, string[]>, baseDir: string, rootDir: string) {
86
+ for (const [key, targets] of Object.entries(paths)) {
87
+ const alias = getAliasName(key)
88
+ if (!alias) continue
89
+
90
+ for (const target of targets) {
91
+ if (!isRootTarget(target, baseDir, rootDir)) continue
92
+ return alias
93
+ }
94
+ }
95
+
96
+ return undefined
97
+ }
98
+
99
+ function getAliasName(pattern: string) {
100
+ const wildcard = splitWildcard(pattern)
101
+ if (!wildcard || wildcard.suffix.length > 0) return undefined
102
+
103
+ const aliasPrefix = wildcard.prefix.replace(/\/$/, "")
104
+ if (!aliasPrefix) return undefined
105
+
106
+ return aliasPrefix
107
+ }
108
+
109
+ function isRootTarget(pattern: string, baseDir: string, rootDir: string) {
110
+ const wildcard = splitWildcard(pattern)
111
+ if (!wildcard || wildcard.suffix.length > 0) return false
112
+
113
+ const targetPath = resolve(baseDir, wildcard.prefix || ".")
114
+
115
+ return normalizeForComparison(targetPath) === normalizeForComparison(rootDir)
116
+ }
117
+
118
+ function splitWildcard(value: string) {
119
+ const index = value.indexOf("*")
120
+ if (index < 0) return undefined
121
+ if (value.indexOf("*", index + 1) >= 0) return undefined
122
+
123
+ return {
124
+ prefix: value.slice(0, index),
125
+ suffix: value.slice(index + 1),
126
+ }
127
+ }
128
+
129
+ async function resolveExtendsPath(extendsValue: string, baseDir: string) {
130
+ if (isAbsolute(extendsValue) || extendsValue.startsWith(".")) {
131
+ const relativeCandidates = getRelativeCandidates(resolve(baseDir, extendsValue))
132
+ for (const candidate of relativeCandidates) {
133
+ if (await exists(candidate)) return candidate
134
+ }
135
+
136
+ return undefined
137
+ }
138
+
139
+ const moduleCandidates = getModuleCandidates(extendsValue)
140
+
141
+ for (const candidate of moduleCandidates) {
142
+ try {
143
+ return require.resolve(candidate, { paths: [baseDir] })
144
+ } catch (error) {}
145
+ }
146
+
147
+ return undefined
148
+ }
149
+
150
+ function getRelativeCandidates(path: string) {
151
+ if (path.endsWith(".json")) return [path]
152
+ return [path, `${path}.json`]
153
+ }
154
+
155
+ function getModuleCandidates(path: string) {
156
+ if (path.endsWith(".json")) return [path]
157
+ return [path, `${path}.json`]
158
+ }
159
+
160
+ async function readJsonConfig(path: string): Promise<JsonObject> {
161
+ const content = await readFile(path, "utf-8")
162
+ const parsed = JSON.parse(stripTrailingCommas(stripComments(content)))
163
+ return toObject(parsed)
164
+ }
165
+
166
+ function stripComments(content: string) {
167
+ const output: string[] = []
168
+ let inString = false
169
+ let escaped = false
170
+
171
+ for (let index = 0; index < content.length; index++) {
172
+ const char = content[index]
173
+ const next = content[index + 1]
174
+
175
+ if (inString) {
176
+ output.push(char)
177
+ if (escaped) {
178
+ escaped = false
179
+ continue
180
+ }
181
+
182
+ if (char === "\\") {
183
+ escaped = true
184
+ continue
185
+ }
186
+
187
+ if (char === "\"") inString = false
188
+
189
+ continue
190
+ }
191
+
192
+ if (char === "\"") {
193
+ inString = true
194
+ output.push(char)
195
+ continue
196
+ }
197
+
198
+ if (char === "/" && next === "/") {
199
+ while (index < content.length && content[index] !== "\n") index++
200
+ output.push("\n")
201
+ continue
202
+ }
203
+
204
+ if (char === "/" && next === "*") {
205
+ index += 2
206
+ while (index < content.length && !(content[index] === "*" && content[index + 1] === "/")) index++
207
+ index++
208
+ continue
209
+ }
210
+
211
+ output.push(char)
212
+ }
213
+
214
+ return output.join("").replace(/^\uFEFF/, "")
215
+ }
216
+
217
+ function stripTrailingCommas(content: string) {
218
+ return content.replace(/,\s*([}\]])/g, "$1")
219
+ }
220
+
221
+ function toPathMap(value: unknown): Record<string, string[]> | undefined {
222
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined
223
+
224
+ const entries: [string, string[]][] = []
225
+
226
+ for (const [key, item] of Object.entries(value)) {
227
+ if (!Array.isArray(item)) continue
228
+ const list = item.filter((target): target is string => typeof target === "string")
229
+ if (list.length === 0) continue
230
+ entries.push([key, list])
231
+ }
232
+
233
+ if (entries.length === 0) return undefined
234
+
235
+ return Object.fromEntries(entries)
236
+ }
237
+
238
+ function toObject(value: unknown): JsonObject {
239
+ if (value && typeof value === "object" && !Array.isArray(value)) return value as JsonObject
240
+ return {}
241
+ }
242
+
243
+ async function exists(path: string) {
244
+ try {
245
+ await access(path, constants.F_OK)
246
+ return true
247
+ } catch (error) {
248
+ return false
249
+ }
250
+ }
251
+
252
+ function normalizeForComparison(path: string) {
253
+ return normalize(path).replace(/\\/g, "/").replace(/\/$/, "").toLowerCase()
254
+ }
255
+
256
+ type JsonObject = Record<string, unknown>