@simplysm/sd-cli 13.0.72 → 13.0.74

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,44 +1,44 @@
1
- import path from "path";
2
- import { createJiti } from "jiti";
3
- import { SdError } from "@simplysm/core-common";
4
- import { fsExists } from "@simplysm/core-node";
5
- import type { SdConfig, SdConfigParams } from "../sd-config.types";
6
-
7
- /**
8
- * Load sd.config.ts
9
- * @returns SdConfig object
10
- * @throws if sd.config.ts is missing or format is incorrect
11
- */
12
- export async function loadSdConfig(params: SdConfigParams): Promise<SdConfig> {
13
- const sdConfigPath = path.resolve(params.cwd, "sd.config.ts");
14
-
15
- if (!(await fsExists(sdConfigPath))) {
16
- throw new SdError(`sd.config.ts file not found: ${sdConfigPath}`);
17
- }
18
-
19
- const jiti = createJiti(import.meta.url);
20
- const sdConfigModule = await jiti.import(sdConfigPath);
21
-
22
- if (
23
- sdConfigModule == null ||
24
- typeof sdConfigModule !== "object" ||
25
- !("default" in sdConfigModule) ||
26
- typeof sdConfigModule.default !== "function"
27
- ) {
28
- throw new SdError(`sd.config.ts must export a function as default: ${sdConfigPath}`);
29
- }
30
-
31
- const config = await sdConfigModule.default(params);
32
-
33
- if (
34
- config == null ||
35
- typeof config !== "object" ||
36
- !("packages" in config) ||
37
- config.packages == null ||
38
- typeof config.packages !== "object" ||
39
- Array.isArray(config.packages)
40
- ) {
41
- throw new SdError(`sd.config.ts return value is not in the correct format: ${sdConfigPath}`);
42
- }
43
- return config as SdConfig;
44
- }
1
+ import path from "path";
2
+ import { createJiti } from "jiti";
3
+ import { SdError } from "@simplysm/core-common";
4
+ import { fsExists } from "@simplysm/core-node";
5
+ import type { SdConfig, SdConfigParams } from "../sd-config.types";
6
+
7
+ /**
8
+ * Load sd.config.ts
9
+ * @returns SdConfig object
10
+ * @throws if sd.config.ts is missing or format is incorrect
11
+ */
12
+ export async function loadSdConfig(params: SdConfigParams): Promise<SdConfig> {
13
+ const sdConfigPath = path.resolve(params.cwd, "sd.config.ts");
14
+
15
+ if (!(await fsExists(sdConfigPath))) {
16
+ throw new SdError(`sd.config.ts file not found: ${sdConfigPath}`);
17
+ }
18
+
19
+ const jiti = createJiti(import.meta.url);
20
+ const sdConfigModule = await jiti.import(sdConfigPath);
21
+
22
+ if (
23
+ sdConfigModule == null ||
24
+ typeof sdConfigModule !== "object" ||
25
+ !("default" in sdConfigModule) ||
26
+ typeof sdConfigModule.default !== "function"
27
+ ) {
28
+ throw new SdError(`sd.config.ts must export a function as default: ${sdConfigPath}`);
29
+ }
30
+
31
+ const config = await sdConfigModule.default(params);
32
+
33
+ if (
34
+ config == null ||
35
+ typeof config !== "object" ||
36
+ !("packages" in config) ||
37
+ config.packages == null ||
38
+ typeof config.packages !== "object" ||
39
+ Array.isArray(config.packages)
40
+ ) {
41
+ throw new SdError(`sd.config.ts return value is not in the correct format: ${sdConfigPath}`);
42
+ }
43
+ return config as SdConfig;
44
+ }
@@ -1,98 +1,98 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- const jsExtensions = [".js", ".cjs", ".mjs"];
5
-
6
- const jsResolutionOrder = ["", ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts", ".jsx", ".tsx"];
7
- const tsResolutionOrder = ["", ".ts", ".cts", ".mts", ".tsx", ".js", ".cjs", ".mjs", ".jsx"];
8
-
9
- function resolveWithExtension(file: string, extensions: string[]): string | null {
10
- for (const ext of extensions) {
11
- const full = `${file}${ext}`;
12
- if (fs.existsSync(full) && fs.statSync(full).isFile()) {
13
- return full;
14
- }
15
- }
16
- for (const ext of extensions) {
17
- const full = `${file}/index${ext}`;
18
- if (fs.existsSync(full) && fs.statSync(full).isFile()) {
19
- return full;
20
- }
21
- }
22
- return null;
23
- }
24
-
25
- function resolvePackageFile(specifier: string, fromDir: string): string | null {
26
- const parts = specifier.split("/");
27
- const pkgName = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
28
- const subPath = specifier.startsWith("@") ? parts.slice(2).join("/") : parts.slice(1).join("/");
29
-
30
- let searchDir = fromDir;
31
- while (true) {
32
- const candidate = path.join(searchDir, "node_modules", pkgName);
33
- if (fs.existsSync(candidate)) {
34
- const realDir = fs.realpathSync(candidate);
35
- if (subPath) {
36
- return resolveWithExtension(path.join(realDir, subPath), tsResolutionOrder);
37
- }
38
- return resolveWithExtension(path.join(realDir, "index"), tsResolutionOrder);
39
- }
40
- const parent = path.dirname(searchDir);
41
- if (parent === searchDir) break;
42
- searchDir = parent;
43
- }
44
- return null;
45
- }
46
-
47
- /**
48
- * Recursively collect dependencies of Tailwind config file
49
- *
50
- * Tailwind built-in `getModuleDependencies` only tracks relative path imports,
51
- * but this function also resolves `node_modules` symlinks to track actual files for packages in specified scope.
52
- */
53
- export function getTailwindConfigDeps(configPath: string, replaceDeps: string[]): string[] {
54
- const seen = new Set<string>();
55
-
56
- function isReplaceDepImport(specifier: string): boolean {
57
- return replaceDeps.some((dep) => specifier === dep || specifier.startsWith(dep + "/"));
58
- }
59
-
60
- function walk(absoluteFile: string): void {
61
- if (seen.has(absoluteFile)) return;
62
- if (!fs.existsSync(absoluteFile)) return;
63
- seen.add(absoluteFile);
64
-
65
- const base = path.dirname(absoluteFile);
66
- const ext = path.extname(absoluteFile);
67
- const extensions = jsExtensions.includes(ext) ? jsResolutionOrder : tsResolutionOrder;
68
-
69
- let contents: string;
70
- try {
71
- contents = fs.readFileSync(absoluteFile, "utf-8");
72
- } catch {
73
- return;
74
- }
75
-
76
- for (const match of [
77
- ...contents.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),
78
- ...contents.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),
79
- ...contents.matchAll(/require\(['"`](.+)['"`]\)/gi),
80
- ]) {
81
- const specifier = match[1];
82
- let resolved: string | null = null;
83
-
84
- if (specifier.startsWith(".")) {
85
- resolved = resolveWithExtension(path.resolve(base, specifier), extensions);
86
- } else if (isReplaceDepImport(specifier)) {
87
- resolved = resolvePackageFile(specifier, base);
88
- }
89
-
90
- if (resolved != null) {
91
- walk(resolved);
92
- }
93
- }
94
- }
95
-
96
- walk(path.resolve(configPath));
97
- return [...seen];
98
- }
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const jsExtensions = [".js", ".cjs", ".mjs"];
5
+
6
+ const jsResolutionOrder = ["", ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts", ".jsx", ".tsx"];
7
+ const tsResolutionOrder = ["", ".ts", ".cts", ".mts", ".tsx", ".js", ".cjs", ".mjs", ".jsx"];
8
+
9
+ function resolveWithExtension(file: string, extensions: string[]): string | null {
10
+ for (const ext of extensions) {
11
+ const full = `${file}${ext}`;
12
+ if (fs.existsSync(full) && fs.statSync(full).isFile()) {
13
+ return full;
14
+ }
15
+ }
16
+ for (const ext of extensions) {
17
+ const full = `${file}/index${ext}`;
18
+ if (fs.existsSync(full) && fs.statSync(full).isFile()) {
19
+ return full;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function resolvePackageFile(specifier: string, fromDir: string): string | null {
26
+ const parts = specifier.split("/");
27
+ const pkgName = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
28
+ const subPath = specifier.startsWith("@") ? parts.slice(2).join("/") : parts.slice(1).join("/");
29
+
30
+ let searchDir = fromDir;
31
+ while (true) {
32
+ const candidate = path.join(searchDir, "node_modules", pkgName);
33
+ if (fs.existsSync(candidate)) {
34
+ const realDir = fs.realpathSync(candidate);
35
+ if (subPath) {
36
+ return resolveWithExtension(path.join(realDir, subPath), tsResolutionOrder);
37
+ }
38
+ return resolveWithExtension(path.join(realDir, "index"), tsResolutionOrder);
39
+ }
40
+ const parent = path.dirname(searchDir);
41
+ if (parent === searchDir) break;
42
+ searchDir = parent;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Recursively collect dependencies of Tailwind config file
49
+ *
50
+ * Tailwind built-in `getModuleDependencies` only tracks relative path imports,
51
+ * but this function also resolves `node_modules` symlinks to track actual files for packages in specified scope.
52
+ */
53
+ export function getTailwindConfigDeps(configPath: string, replaceDeps: string[]): string[] {
54
+ const seen = new Set<string>();
55
+
56
+ function isReplaceDepImport(specifier: string): boolean {
57
+ return replaceDeps.some((dep) => specifier === dep || specifier.startsWith(dep + "/"));
58
+ }
59
+
60
+ function walk(absoluteFile: string): void {
61
+ if (seen.has(absoluteFile)) return;
62
+ if (!fs.existsSync(absoluteFile)) return;
63
+ seen.add(absoluteFile);
64
+
65
+ const base = path.dirname(absoluteFile);
66
+ const ext = path.extname(absoluteFile);
67
+ const extensions = jsExtensions.includes(ext) ? jsResolutionOrder : tsResolutionOrder;
68
+
69
+ let contents: string;
70
+ try {
71
+ contents = fs.readFileSync(absoluteFile, "utf-8");
72
+ } catch {
73
+ return;
74
+ }
75
+
76
+ for (const match of [
77
+ ...contents.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),
78
+ ...contents.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),
79
+ ...contents.matchAll(/require\(['"`](.+)['"`]\)/gi),
80
+ ]) {
81
+ const specifier = match[1];
82
+ let resolved: string | null = null;
83
+
84
+ if (specifier.startsWith(".")) {
85
+ resolved = resolveWithExtension(path.resolve(base, specifier), extensions);
86
+ } else if (isReplaceDepImport(specifier)) {
87
+ resolved = resolvePackageFile(specifier, base);
88
+ }
89
+
90
+ if (resolved != null) {
91
+ walk(resolved);
92
+ }
93
+ }
94
+ }
95
+
96
+ walk(path.resolve(configPath));
97
+ return [...seen];
98
+ }
@@ -1,56 +1,56 @@
1
- import path from "path";
2
- import Handlebars from "handlebars";
3
- import { fsCopy, fsMkdir, fsRead, fsReaddir, fsStat, fsWrite } from "@simplysm/core-node";
4
-
5
- /**
6
- * Recursively traverse template directory, render with Handlebars, and generate files
7
- *
8
- * - `.hbs` extension files: Compile with Handlebars → save with `.hbs` removed from name
9
- * - If `.hbs` result is empty string/whitespace only: skip file creation
10
- * - Other files: copy as-is (binary)
11
- *
12
- * @param srcDir - Template source directory
13
- * @param destDir - Output destination directory
14
- * @param context - Handlebars template variables
15
- * @param dirReplacements - Directory name substitution map (e.g., `{ __CLIENT__: "client-admin" }`)
16
- */
17
- export async function renderTemplateDir(
18
- srcDir: string,
19
- destDir: string,
20
- context: Record<string, unknown>,
21
- dirReplacements?: Record<string, string>,
22
- ): Promise<void> {
23
- await fsMkdir(destDir);
24
-
25
- const entries = await fsReaddir(srcDir);
26
-
27
- for (const entry of entries) {
28
- const srcPath = path.join(srcDir, entry);
29
- const stat = await fsStat(srcPath);
30
-
31
- if (stat.isDirectory()) {
32
- // Apply directory name substitution
33
- const destName = dirReplacements?.[entry] ?? entry;
34
- await renderTemplateDir(
35
- path.join(srcDir, entry),
36
- path.join(destDir, destName),
37
- context,
38
- dirReplacements,
39
- );
40
- } else if (entry.endsWith(".hbs")) {
41
- // Render Handlebars template
42
- const source = await fsRead(srcPath);
43
- const template = Handlebars.compile(source, { noEscape: true });
44
- const result = template(context);
45
-
46
- // Skip file creation if result is empty or whitespace-only
47
- if (result.trim().length === 0) continue;
48
-
49
- const destFileName = entry.slice(0, -4); // Remove .hbs
50
- await fsWrite(path.join(destDir, destFileName), result);
51
- } else {
52
- // Copy binary files as-is
53
- await fsCopy(srcPath, path.join(destDir, entry));
54
- }
55
- }
56
- }
1
+ import path from "path";
2
+ import Handlebars from "handlebars";
3
+ import { fsCopy, fsMkdir, fsRead, fsReaddir, fsStat, fsWrite } from "@simplysm/core-node";
4
+
5
+ /**
6
+ * Recursively traverse template directory, render with Handlebars, and generate files
7
+ *
8
+ * - `.hbs` extension files: Compile with Handlebars → save with `.hbs` removed from name
9
+ * - If `.hbs` result is empty string/whitespace only: skip file creation
10
+ * - Other files: copy as-is (binary)
11
+ *
12
+ * @param srcDir - Template source directory
13
+ * @param destDir - Output destination directory
14
+ * @param context - Handlebars template variables
15
+ * @param dirReplacements - Directory name substitution map (e.g., `{ __CLIENT__: "client-admin" }`)
16
+ */
17
+ export async function renderTemplateDir(
18
+ srcDir: string,
19
+ destDir: string,
20
+ context: Record<string, unknown>,
21
+ dirReplacements?: Record<string, string>,
22
+ ): Promise<void> {
23
+ await fsMkdir(destDir);
24
+
25
+ const entries = await fsReaddir(srcDir);
26
+
27
+ for (const entry of entries) {
28
+ const srcPath = path.join(srcDir, entry);
29
+ const stat = await fsStat(srcPath);
30
+
31
+ if (stat.isDirectory()) {
32
+ // Apply directory name substitution
33
+ const destName = dirReplacements?.[entry] ?? entry;
34
+ await renderTemplateDir(
35
+ path.join(srcDir, entry),
36
+ path.join(destDir, destName),
37
+ context,
38
+ dirReplacements,
39
+ );
40
+ } else if (entry.endsWith(".hbs")) {
41
+ // Render Handlebars template
42
+ const source = await fsRead(srcPath);
43
+ const template = Handlebars.compile(source, { noEscape: true });
44
+ const result = template(context);
45
+
46
+ // Skip file creation if result is empty or whitespace-only
47
+ if (result.trim().length === 0) continue;
48
+
49
+ const destFileName = entry.slice(0, -4); // Remove .hbs
50
+ await fsWrite(path.join(destDir, destFileName), result);
51
+ } else {
52
+ // Copy binary files as-is
53
+ await fsCopy(srcPath, path.join(destDir, entry));
54
+ }
55
+ }
56
+ }
@@ -1,127 +1,127 @@
1
- import ts from "typescript";
2
- import path from "path";
3
- import { fsExists, fsReadJson, pathIsChildPath } from "@simplysm/core-node";
4
- import { SdError } from "@simplysm/core-common";
5
-
6
- /**
7
- * DOM-related lib patterns - libs that include browser APIs
8
- * Used when filtering libs that should be excluded from node environment (lib.dom.d.ts, lib.webworker.d.ts, etc)
9
- */
10
- const DOM_LIB_PATTERNS = ["dom", "webworker"] as const;
11
-
12
- /**
13
- * Read @types/* devDependencies from package.json and return types list
14
- */
15
- export async function getTypesFromPackageJson(packageDir: string): Promise<string[]> {
16
- const packageJsonPath = path.join(packageDir, "package.json");
17
- if (!(await fsExists(packageJsonPath))) {
18
- return [];
19
- }
20
-
21
- const packageJson = await fsReadJson<{ devDependencies?: Record<string, string> }>(
22
- packageJsonPath,
23
- );
24
- const devDeps = packageJson.devDependencies ?? {};
25
-
26
- return Object.keys(devDeps)
27
- .filter((dep) => dep.startsWith("@types/"))
28
- .map((dep) => dep.replace("@types/", ""));
29
- }
30
-
31
- /**
32
- * Type check environment
33
- * - node: remove DOM lib + add node types
34
- * - browser: remove node types
35
- * - neutral: keep DOM lib + add node types (for Node/browser shared packages)
36
- */
37
- export type TypecheckEnv = "node" | "browser" | "neutral";
38
-
39
- /**
40
- * Create compiler options for package
41
- *
42
- * @param baseOptions - Compiler options from root tsconfig
43
- * @param env - Type check environment (node: remove DOM lib + add node types, browser: remove node types)
44
- * @param packageDir - Package directory path
45
- *
46
- * @remarks
47
- * The types option ignores baseOptions.types and is newly constructed per package.
48
- * This is because the global types in root tsconfig may not fit the package environment.
49
- * (e.g., prevent node types from being included in browser packages)
50
- */
51
- export async function getCompilerOptionsForPackage(
52
- baseOptions: ts.CompilerOptions,
53
- env: TypecheckEnv,
54
- packageDir: string,
55
- ): Promise<ts.CompilerOptions> {
56
- const options = { ...baseOptions };
57
- const packageTypes = await getTypesFromPackageJson(packageDir);
58
-
59
- // pnpm environment: search both package-specific node_modules/@types and root node_modules/@types
60
- options.typeRoots = [
61
- path.join(packageDir, "node_modules", "@types"),
62
- path.join(process.cwd(), "node_modules", "@types"),
63
- ];
64
-
65
- switch (env) {
66
- case "node":
67
- options.lib = options.lib?.filter(
68
- (lib) => !DOM_LIB_PATTERNS.some((pattern) => lib.toLowerCase().includes(pattern)),
69
- );
70
- options.types = [...new Set([...packageTypes, "node"])];
71
- break;
72
- case "browser":
73
- options.types = packageTypes.filter((t) => t !== "node");
74
- break;
75
- case "neutral":
76
- options.types = [...new Set([...packageTypes, "node"])];
77
- break;
78
- }
79
-
80
- return options;
81
- }
82
-
83
- /**
84
- * Parse root tsconfig
85
- * @throws If unable to read or parse tsconfig.json
86
- */
87
- export function parseRootTsconfig(cwd: string): ts.ParsedCommandLine {
88
- const tsconfigPath = path.join(cwd, "tsconfig.json");
89
- const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
90
-
91
- if (configFile.error) {
92
- const message = ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
93
- throw new SdError(`Failed to read tsconfig.json: ${message}`);
94
- }
95
-
96
- const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, cwd);
97
-
98
- if (parsed.errors.length > 0) {
99
- const messages = parsed.errors.map((e) => ts.flattenDiagnosticMessageText(e.messageText, "\n"));
100
- throw new SdError(`Failed to parse tsconfig.json: ${messages.join("; ")}`);
101
- }
102
-
103
- return parsed;
104
- }
105
-
106
- /**
107
- * Get list of package source files (based on tsconfig)
108
- */
109
- export function getPackageSourceFiles(
110
- pkgDir: string,
111
- parsedConfig: ts.ParsedCommandLine,
112
- ): string[] {
113
- const pkgSrcDir = path.join(pkgDir, "src");
114
- return parsedConfig.fileNames.filter((f) => pathIsChildPath(f, pkgSrcDir));
115
- }
116
-
117
- /**
118
- * Get full list of package files (including src + tests)
119
- */
120
- export function getPackageFiles(pkgDir: string, parsedConfig: ts.ParsedCommandLine): string[] {
121
- return parsedConfig.fileNames.filter((f) => {
122
- if (!pathIsChildPath(f, pkgDir)) return false;
123
- // Exclude files directly in package root (config files) — treated same as project root files in other tasks
124
- const relative = path.relative(pkgDir, f);
125
- return path.dirname(relative) !== ".";
126
- });
127
- }
1
+ import ts from "typescript";
2
+ import path from "path";
3
+ import { fsExists, fsReadJson, pathIsChildPath } from "@simplysm/core-node";
4
+ import { SdError } from "@simplysm/core-common";
5
+
6
+ /**
7
+ * DOM-related lib patterns - libs that include browser APIs
8
+ * Used when filtering libs that should be excluded from node environment (lib.dom.d.ts, lib.webworker.d.ts, etc)
9
+ */
10
+ const DOM_LIB_PATTERNS = ["dom", "webworker"] as const;
11
+
12
+ /**
13
+ * Read @types/* devDependencies from package.json and return types list
14
+ */
15
+ export async function getTypesFromPackageJson(packageDir: string): Promise<string[]> {
16
+ const packageJsonPath = path.join(packageDir, "package.json");
17
+ if (!(await fsExists(packageJsonPath))) {
18
+ return [];
19
+ }
20
+
21
+ const packageJson = await fsReadJson<{ devDependencies?: Record<string, string> }>(
22
+ packageJsonPath,
23
+ );
24
+ const devDeps = packageJson.devDependencies ?? {};
25
+
26
+ return Object.keys(devDeps)
27
+ .filter((dep) => dep.startsWith("@types/"))
28
+ .map((dep) => dep.replace("@types/", ""));
29
+ }
30
+
31
+ /**
32
+ * Type check environment
33
+ * - node: remove DOM lib + add node types
34
+ * - browser: remove node types
35
+ * - neutral: keep DOM lib + add node types (for Node/browser shared packages)
36
+ */
37
+ export type TypecheckEnv = "node" | "browser" | "neutral";
38
+
39
+ /**
40
+ * Create compiler options for package
41
+ *
42
+ * @param baseOptions - Compiler options from root tsconfig
43
+ * @param env - Type check environment (node: remove DOM lib + add node types, browser: remove node types)
44
+ * @param packageDir - Package directory path
45
+ *
46
+ * @remarks
47
+ * The types option ignores baseOptions.types and is newly constructed per package.
48
+ * This is because the global types in root tsconfig may not fit the package environment.
49
+ * (e.g., prevent node types from being included in browser packages)
50
+ */
51
+ export async function getCompilerOptionsForPackage(
52
+ baseOptions: ts.CompilerOptions,
53
+ env: TypecheckEnv,
54
+ packageDir: string,
55
+ ): Promise<ts.CompilerOptions> {
56
+ const options = { ...baseOptions };
57
+ const packageTypes = await getTypesFromPackageJson(packageDir);
58
+
59
+ // pnpm environment: search both package-specific node_modules/@types and root node_modules/@types
60
+ options.typeRoots = [
61
+ path.join(packageDir, "node_modules", "@types"),
62
+ path.join(process.cwd(), "node_modules", "@types"),
63
+ ];
64
+
65
+ switch (env) {
66
+ case "node":
67
+ options.lib = options.lib?.filter(
68
+ (lib) => !DOM_LIB_PATTERNS.some((pattern) => lib.toLowerCase().includes(pattern)),
69
+ );
70
+ options.types = [...new Set([...packageTypes, "node"])];
71
+ break;
72
+ case "browser":
73
+ options.types = packageTypes.filter((t) => t !== "node");
74
+ break;
75
+ case "neutral":
76
+ options.types = [...new Set([...packageTypes, "node"])];
77
+ break;
78
+ }
79
+
80
+ return options;
81
+ }
82
+
83
+ /**
84
+ * Parse root tsconfig
85
+ * @throws If unable to read or parse tsconfig.json
86
+ */
87
+ export function parseRootTsconfig(cwd: string): ts.ParsedCommandLine {
88
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
89
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
90
+
91
+ if (configFile.error) {
92
+ const message = ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
93
+ throw new SdError(`Failed to read tsconfig.json: ${message}`);
94
+ }
95
+
96
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, cwd);
97
+
98
+ if (parsed.errors.length > 0) {
99
+ const messages = parsed.errors.map((e) => ts.flattenDiagnosticMessageText(e.messageText, "\n"));
100
+ throw new SdError(`Failed to parse tsconfig.json: ${messages.join("; ")}`);
101
+ }
102
+
103
+ return parsed;
104
+ }
105
+
106
+ /**
107
+ * Get list of package source files (based on tsconfig)
108
+ */
109
+ export function getPackageSourceFiles(
110
+ pkgDir: string,
111
+ parsedConfig: ts.ParsedCommandLine,
112
+ ): string[] {
113
+ const pkgSrcDir = path.join(pkgDir, "src");
114
+ return parsedConfig.fileNames.filter((f) => pathIsChildPath(f, pkgSrcDir));
115
+ }
116
+
117
+ /**
118
+ * Get full list of package files (including src + tests)
119
+ */
120
+ export function getPackageFiles(pkgDir: string, parsedConfig: ts.ParsedCommandLine): string[] {
121
+ return parsedConfig.fileNames.filter((f) => {
122
+ if (!pathIsChildPath(f, pkgDir)) return false;
123
+ // Exclude files directly in package root (config files) — treated same as project root files in other tasks
124
+ const relative = path.relative(pkgDir, f);
125
+ return path.dirname(relative) !== ".";
126
+ });
127
+ }