@nuasite/cms 0.23.1 → 0.24.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/editor.js
CHANGED
|
@@ -381,7 +381,7 @@ function CS(t, e) {
|
|
|
381
381
|
function ES(t, e) {
|
|
382
382
|
return typeof e == "function" ? e(t) : e;
|
|
383
383
|
}
|
|
384
|
-
const J_ = "0.
|
|
384
|
+
const J_ = "0.24.0", j_ = J_, ct = {
|
|
385
385
|
/** Highlight overlay for hovered elements */
|
|
386
386
|
HIGHLIGHT: 2147483644,
|
|
387
387
|
/** Hover outline for elements/components */
|
package/package.json
CHANGED
package/src/build-processor.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from './source-finder'
|
|
25
25
|
import type { ComponentInstance } from './types'
|
|
26
26
|
import type { CmsMarkerOptions, CollectionEntry } from './types'
|
|
27
|
-
import { firstNonEmptyLine } from './utils'
|
|
27
|
+
import { firstNonEmptyLine, resolveSourcePath } from './utils'
|
|
28
28
|
|
|
29
29
|
// Concurrency limit for parallel processing
|
|
30
30
|
const MAX_CONCURRENT = 10
|
|
@@ -425,9 +425,7 @@ async function processFile(
|
|
|
425
425
|
|
|
426
426
|
// Update attribute and colorClasses source information if we have an opening tag
|
|
427
427
|
if (sourceLocation.openingTagSnippet) {
|
|
428
|
-
const filePath =
|
|
429
|
-
? sourceLocation.file
|
|
430
|
-
: path.join(getProjectRoot(), sourceLocation.file)
|
|
428
|
+
const filePath = resolveSourcePath(sourceLocation.file)
|
|
431
429
|
try {
|
|
432
430
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
433
431
|
const lines = content.split('\n')
|
|
@@ -575,6 +575,54 @@ export async function findFieldInCollectionEntry(
|
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Find multiple fields by name in a specific collection entry's data file.
|
|
580
|
+
* Parses the YAML only once, unlike calling findFieldInCollectionEntry per field.
|
|
581
|
+
*/
|
|
582
|
+
export async function findFieldsInCollectionEntry(
|
|
583
|
+
fieldNames: Set<string>,
|
|
584
|
+
collectionName: string,
|
|
585
|
+
collectionSlug: string,
|
|
586
|
+
collectionDefinitions: Record<string, CollectionDefinition>,
|
|
587
|
+
): Promise<Map<string, SourceLocation>> {
|
|
588
|
+
const def = collectionDefinitions[collectionName]
|
|
589
|
+
if (!def?.entries) return new Map()
|
|
590
|
+
|
|
591
|
+
const entry = def.entries.find((e) => e.slug === collectionSlug)
|
|
592
|
+
if (!entry) return new Map()
|
|
593
|
+
|
|
594
|
+
const info: CollectionInfo = { name: collectionName, slug: collectionSlug, file: entry.sourcePath }
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const filePath = path.join(getProjectRoot(), entry.sourcePath)
|
|
598
|
+
const cached = await getCachedMarkdownFile(filePath)
|
|
599
|
+
if (!cached) return new Map()
|
|
600
|
+
|
|
601
|
+
if (def.type === 'data') {
|
|
602
|
+
return findFieldsByNameInYaml(cached.content, 0, fieldNames, cached.lines, info)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// For markdown, search inside frontmatter only
|
|
606
|
+
const { lines } = cached
|
|
607
|
+
let fmStart = -1
|
|
608
|
+
let fmEnd = -1
|
|
609
|
+
for (let i = 0; i < lines.length; i++) {
|
|
610
|
+
if (lines[i]?.trim() === '---') {
|
|
611
|
+
if (fmStart === -1) fmStart = i
|
|
612
|
+
else {
|
|
613
|
+
fmEnd = i
|
|
614
|
+
break
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (fmEnd <= 0) return new Map()
|
|
619
|
+
const yamlStr = lines.slice(fmStart + 1, fmEnd).join('\n')
|
|
620
|
+
return findFieldsByNameInYaml(yamlStr, fmStart + 1, fieldNames, lines, info)
|
|
621
|
+
} catch {
|
|
622
|
+
return new Map()
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
578
626
|
/**
|
|
579
627
|
* Walk a YAML AST to find a field by key name (regardless of its value).
|
|
580
628
|
*/
|
|
@@ -585,13 +633,30 @@ function findFieldByNameInYaml(
|
|
|
585
633
|
fileLines: string[],
|
|
586
634
|
collectionInfo: CollectionInfo,
|
|
587
635
|
): SourceLocation | undefined {
|
|
636
|
+
const results = findFieldsByNameInYaml(yamlStr, lineOffset, new Set([fieldName]), fileLines, collectionInfo)
|
|
637
|
+
return results.get(fieldName)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Walk a YAML AST to find multiple fields by key name in a single parse.
|
|
642
|
+
* Returns a map of fieldName → SourceLocation for all matched fields.
|
|
643
|
+
*/
|
|
644
|
+
function findFieldsByNameInYaml(
|
|
645
|
+
yamlStr: string,
|
|
646
|
+
lineOffset: number,
|
|
647
|
+
fieldNames: Set<string>,
|
|
648
|
+
fileLines: string[],
|
|
649
|
+
collectionInfo: CollectionInfo,
|
|
650
|
+
): Map<string, SourceLocation> {
|
|
588
651
|
const lineCounter = new LineCounter()
|
|
589
652
|
const doc = parseDocument(yamlStr, { lineCounter })
|
|
590
|
-
|
|
653
|
+
const results = new Map<string, SourceLocation>()
|
|
654
|
+
if (!isMap(doc.contents)) return results
|
|
591
655
|
|
|
592
656
|
for (const pair of doc.contents.items) {
|
|
593
657
|
if (!isPair(pair) || !isScalar(pair.key)) continue
|
|
594
|
-
|
|
658
|
+
const key = String(pair.key.value)
|
|
659
|
+
if (!fieldNames.has(key)) continue
|
|
595
660
|
if (!isScalar(pair.value)) continue
|
|
596
661
|
|
|
597
662
|
const keyRange = (pair.key as any).range as [number, number, number] | undefined
|
|
@@ -600,17 +665,20 @@ function findFieldByNameInYaml(
|
|
|
600
665
|
const endLine = (valRange ? lineCounter.linePos(valRange[1]).line : startLine - lineOffset) + lineOffset
|
|
601
666
|
|
|
602
667
|
const snippet = fileLines.slice(startLine - 1, endLine).join('\n')
|
|
603
|
-
|
|
668
|
+
results.set(key, {
|
|
604
669
|
file: collectionInfo.file,
|
|
605
670
|
line: startLine,
|
|
606
671
|
snippet,
|
|
607
672
|
type: 'collection',
|
|
608
|
-
variableName:
|
|
673
|
+
variableName: key,
|
|
609
674
|
collectionName: collectionInfo.name,
|
|
610
675
|
collectionSlug: collectionInfo.slug,
|
|
611
|
-
}
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
// Early exit if all fields found
|
|
679
|
+
if (results.size === fieldNames.size) break
|
|
612
680
|
}
|
|
613
|
-
return
|
|
681
|
+
return results
|
|
614
682
|
}
|
|
615
683
|
|
|
616
684
|
// ============================================================================
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { getProjectRoot } from '../config'
|
|
5
|
-
import { escapeRegex } from '../utils'
|
|
5
|
+
import { escapeRegex, resolveSourcePath } from '../utils'
|
|
6
6
|
import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
|
|
7
7
|
import { getCachedParsedFile } from './ast-parser'
|
|
8
8
|
import { findComponentProp, findExpressionProp, findSpreadProp } from './element-finder'
|
|
@@ -384,9 +384,7 @@ export async function findAttributeSourceLocation(
|
|
|
384
384
|
// Get the property name (last part of the expression)
|
|
385
385
|
const propName = exprPath.includes('.') ? exprPath.split('.').pop()! : exprPath
|
|
386
386
|
|
|
387
|
-
const filePath =
|
|
388
|
-
? sourceFilePath
|
|
389
|
-
: path.join(getProjectRoot(), sourceFilePath)
|
|
387
|
+
const filePath = resolveSourcePath(sourceFilePath)
|
|
390
388
|
|
|
391
389
|
const cached = await getCachedParsedFile(filePath)
|
|
392
390
|
if (!cached) return undefined
|
|
@@ -4,10 +4,16 @@ import { parse as parseYaml } from 'yaml'
|
|
|
4
4
|
|
|
5
5
|
import { getProjectRoot } from '../config'
|
|
6
6
|
import type { Attribute, CollectionDefinition, ManifestEntry } from '../types'
|
|
7
|
-
import { escapeRegex, generateSourceHash } from '../utils'
|
|
7
|
+
import { escapeRegex, generateSourceHash, resolveSourcePath } from '../utils'
|
|
8
8
|
import { buildDefinitionPath } from './ast-extractors'
|
|
9
9
|
import { getCachedParsedFile } from './ast-parser'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildCollectionTextIndex,
|
|
12
|
+
findFieldInCollectionEntry,
|
|
13
|
+
findFieldsInCollectionEntry,
|
|
14
|
+
findTextInAnyCollectionFrontmatter,
|
|
15
|
+
lookupCollectionText,
|
|
16
|
+
} from './collection-finder'
|
|
11
17
|
import { findAttributeSourceLocation, searchForExpressionProp, searchForPropInParents } from './cross-file-tracker'
|
|
12
18
|
import { findImageElementNearLine, findImageSourceLocation } from './image-finder'
|
|
13
19
|
import { initializeSearchIndex } from './search-index'
|
|
@@ -444,9 +450,7 @@ export async function extractSourceSnippet(
|
|
|
444
450
|
tag: string,
|
|
445
451
|
): Promise<string | undefined> {
|
|
446
452
|
try {
|
|
447
|
-
const filePath =
|
|
448
|
-
? sourceFile
|
|
449
|
-
: path.join(getProjectRoot(), sourceFile)
|
|
453
|
+
const filePath = resolveSourcePath(sourceFile)
|
|
450
454
|
|
|
451
455
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
452
456
|
const lines = content.split('\n')
|
|
@@ -583,9 +587,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
583
587
|
|
|
584
588
|
// Also update attribute and colorClasses source info from the opening tag
|
|
585
589
|
try {
|
|
586
|
-
const filePath =
|
|
587
|
-
? imageLocation.file
|
|
588
|
-
: path.join(getProjectRoot(), imageLocation.file)
|
|
590
|
+
const filePath = resolveSourcePath(imageLocation.file)
|
|
589
591
|
const { lines } = await readFileWithCache(filePath)
|
|
590
592
|
const openingTagInfo = extractOpeningTagWithLine(lines, imageLocation.line - 1, entry.tag)
|
|
591
593
|
|
|
@@ -620,9 +622,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
620
622
|
// Fallback for expression-based src attributes (src={variable})
|
|
621
623
|
if (entry.sourcePath && entry.sourceLine) {
|
|
622
624
|
try {
|
|
623
|
-
const filePath =
|
|
624
|
-
? entry.sourcePath
|
|
625
|
-
: path.join(getProjectRoot(), entry.sourcePath)
|
|
625
|
+
const filePath = resolveSourcePath(entry.sourcePath)
|
|
626
626
|
const cached = await getCachedParsedFile(filePath)
|
|
627
627
|
if (cached) {
|
|
628
628
|
const nearbyImg = findImageElementNearLine(cached.ast, entry.sourceLine, cached.lines)
|
|
@@ -662,6 +662,18 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
662
662
|
return [id, entry] as const
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
// Collection text: resolve directly from the data file
|
|
666
|
+
if (entry.text?.trim() && entry.collectionName && entry.collectionSlug && collectionDefinitions) {
|
|
667
|
+
const textLocation = await resolveCollectionTextField(
|
|
668
|
+
entry,
|
|
669
|
+
collectionDefinitions,
|
|
670
|
+
referenceIndex,
|
|
671
|
+
)
|
|
672
|
+
if (textLocation) {
|
|
673
|
+
return [id, textLocation] as const
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
665
677
|
// Skip if already has sourceSnippet or missing source info
|
|
666
678
|
if (entry.sourceSnippet || !entry.sourcePath || !entry.sourceLine || !entry.tag) {
|
|
667
679
|
return [id, entry] as const
|
|
@@ -669,9 +681,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
669
681
|
|
|
670
682
|
// Read file once and extract both snippets
|
|
671
683
|
try {
|
|
672
|
-
const filePath =
|
|
673
|
-
? entry.sourcePath
|
|
674
|
-
: path.join(getProjectRoot(), entry.sourcePath)
|
|
684
|
+
const filePath = resolveSourcePath(entry.sourcePath)
|
|
675
685
|
|
|
676
686
|
const { content, lines } = await readFileWithCache(filePath)
|
|
677
687
|
|
|
@@ -973,36 +983,33 @@ async function resolveCollectionImageField(
|
|
|
973
983
|
return undefined
|
|
974
984
|
}
|
|
975
985
|
|
|
976
|
-
// Multiple image fields —
|
|
986
|
+
// Multiple image fields — fetch all in one YAML parse, then match by value
|
|
977
987
|
const imgSrc = entry.imageMetadata!.src
|
|
988
|
+
const allResults = await findFieldsInCollectionEntry(
|
|
989
|
+
new Set(imageFields.map(f => f.name)),
|
|
990
|
+
entry.collectionName!,
|
|
991
|
+
entry.collectionSlug!,
|
|
992
|
+
collectionDefinitions,
|
|
993
|
+
)
|
|
994
|
+
|
|
978
995
|
let firstFieldResult: SourceLocation | undefined
|
|
979
996
|
for (const field of imageFields) {
|
|
980
|
-
const fieldResult =
|
|
981
|
-
field.name,
|
|
982
|
-
entry.collectionName!,
|
|
983
|
-
entry.collectionSlug!,
|
|
984
|
-
collectionDefinitions,
|
|
985
|
-
)
|
|
997
|
+
const fieldResult = allResults.get(field.name)
|
|
986
998
|
if (!fieldResult?.snippet) continue
|
|
987
999
|
|
|
988
|
-
// Remember the first resolved field as fallback
|
|
989
1000
|
firstFieldResult ??= fieldResult
|
|
990
1001
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
if (
|
|
997
|
-
|
|
998
|
-
const value = key ? (parsed as Record<string, unknown>)[key] : undefined
|
|
999
|
-
if (typeof value === 'string' && (value === imgSrc || imgSrc.includes(value) || value.includes(imgSrc))) {
|
|
1000
|
-
return applyCollectionSource(entry, fieldResult, referenceIndex)
|
|
1001
|
-
}
|
|
1002
|
+
try {
|
|
1003
|
+
const cleaned = fieldResult.snippet.replace(/,\s*$/, '')
|
|
1004
|
+
const parsed = parseYaml(cleaned)
|
|
1005
|
+
if (parsed && typeof parsed === 'object') {
|
|
1006
|
+
const value = (parsed as Record<string, unknown>)[field.name]
|
|
1007
|
+
if (typeof value === 'string' && (value === imgSrc || imgSrc.includes(value) || value.includes(imgSrc))) {
|
|
1008
|
+
return applyCollectionSource(entry, fieldResult, referenceIndex)
|
|
1002
1009
|
}
|
|
1003
|
-
} catch {
|
|
1004
|
-
// Not valid YAML
|
|
1005
1010
|
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
// Not valid YAML/JSON
|
|
1006
1013
|
}
|
|
1007
1014
|
}
|
|
1008
1015
|
|
|
@@ -1014,6 +1021,110 @@ async function resolveCollectionImageField(
|
|
|
1014
1021
|
return undefined
|
|
1015
1022
|
}
|
|
1016
1023
|
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
// Collection Text Resolution
|
|
1026
|
+
// ============================================================================
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Resolve a collection text entry directly from the data file.
|
|
1030
|
+
* Two strategies, tried in order:
|
|
1031
|
+
*
|
|
1032
|
+
* 1. **Source-map** — read the template expression (e.g., {post.data.title}),
|
|
1033
|
+
* extract the field name, look it up by name in the data file.
|
|
1034
|
+
* 2. **Value match** — iterate over collection fields and compare rendered
|
|
1035
|
+
* text against field values. Handles static/hardcoded text that exists
|
|
1036
|
+
* in both the template and a collection data file.
|
|
1037
|
+
*/
|
|
1038
|
+
async function resolveCollectionTextField(
|
|
1039
|
+
entry: ManifestEntry,
|
|
1040
|
+
collectionDefinitions: Record<string, CollectionDefinition>,
|
|
1041
|
+
referenceIndex?: Map<string, Array<{ collection: string; fieldName: string; isArray?: boolean }>>,
|
|
1042
|
+
): Promise<ManifestEntry | undefined> {
|
|
1043
|
+
const colDef = collectionDefinitions[entry.collectionName!]
|
|
1044
|
+
if (!colDef) return undefined
|
|
1045
|
+
|
|
1046
|
+
// Try template expression as source map (e.g., {post.data.title} → "title")
|
|
1047
|
+
const fieldNames = await extractDataFieldNames(entry)
|
|
1048
|
+
if (fieldNames.size === 1) {
|
|
1049
|
+
const fieldResult = await findFieldInCollectionEntry(
|
|
1050
|
+
fieldNames.values().next().value!,
|
|
1051
|
+
entry.collectionName!,
|
|
1052
|
+
entry.collectionSlug!,
|
|
1053
|
+
collectionDefinitions,
|
|
1054
|
+
)
|
|
1055
|
+
if (fieldResult) {
|
|
1056
|
+
return applyCollectionSource(entry, fieldResult, referenceIndex, { allowStyling: false })
|
|
1057
|
+
}
|
|
1058
|
+
} else if (fieldNames.size > 1) {
|
|
1059
|
+
const result = await matchFieldByValue(entry, fieldNames, collectionDefinitions, referenceIndex)
|
|
1060
|
+
if (result) return result
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Fallback: match rendered text against all non-image field values
|
|
1064
|
+
const allFieldNames = new Set(colDef.fields.filter(f => f.type !== 'image').map(f => f.name))
|
|
1065
|
+
if (allFieldNames.size > 0) {
|
|
1066
|
+
return matchFieldByValue(entry, allFieldNames, collectionDefinitions, referenceIndex)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return undefined
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Extract .data.fieldName references from the template expression at the entry's source location.
|
|
1074
|
+
* Returns an empty set if the entry lacks source info or the template has no data field expressions.
|
|
1075
|
+
*/
|
|
1076
|
+
async function extractDataFieldNames(entry: ManifestEntry): Promise<Set<string>> {
|
|
1077
|
+
const fieldNames = new Set<string>()
|
|
1078
|
+
if (!entry.sourcePath || !entry.sourceLine || !entry.tag) return fieldNames
|
|
1079
|
+
|
|
1080
|
+
const cached = await getCachedParsedFile(resolveSourcePath(entry.sourcePath))
|
|
1081
|
+
if (!cached) return fieldNames
|
|
1082
|
+
|
|
1083
|
+
const snippet = extractCompleteTagSnippet(cached.lines, entry.sourceLine - 1, entry.tag)
|
|
1084
|
+
if (!snippet) return fieldNames
|
|
1085
|
+
|
|
1086
|
+
let match: RegExpExecArray | null
|
|
1087
|
+
const pattern = /\.data\.(\w+)/g
|
|
1088
|
+
while ((match = pattern.exec(snippet)) !== null) {
|
|
1089
|
+
fieldNames.add(match[1]!)
|
|
1090
|
+
}
|
|
1091
|
+
return fieldNames
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Match entry text against collection field values to find the source field. */
|
|
1095
|
+
async function matchFieldByValue(
|
|
1096
|
+
entry: ManifestEntry,
|
|
1097
|
+
fieldNames: Set<string>,
|
|
1098
|
+
collectionDefinitions: Record<string, CollectionDefinition>,
|
|
1099
|
+
referenceIndex?: Map<string, Array<{ collection: string; fieldName: string; isArray?: boolean }>>,
|
|
1100
|
+
): Promise<ManifestEntry | undefined> {
|
|
1101
|
+
const normalizedText = normalizeText(entry.text!)
|
|
1102
|
+
const fieldResults = await findFieldsInCollectionEntry(
|
|
1103
|
+
fieldNames,
|
|
1104
|
+
entry.collectionName!,
|
|
1105
|
+
entry.collectionSlug!,
|
|
1106
|
+
collectionDefinitions,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
for (const [fieldName, fieldResult] of fieldResults) {
|
|
1110
|
+
if (!fieldResult.snippet) continue
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
const cleaned = fieldResult.snippet.replace(/,\s*$/, '')
|
|
1114
|
+
const parsed = parseYaml(cleaned)
|
|
1115
|
+
if (parsed && typeof parsed === 'object') {
|
|
1116
|
+
const value = (parsed as Record<string, unknown>)[fieldName]
|
|
1117
|
+
if (typeof value === 'string' && normalizeText(value) === normalizedText) {
|
|
1118
|
+
return applyCollectionSource(entry, fieldResult, referenceIndex, { allowStyling: false })
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
} catch {
|
|
1122
|
+
// Not valid YAML/JSON
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return undefined
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1017
1128
|
// ============================================================================
|
|
1018
1129
|
// Image Expression Resolution
|
|
1019
1130
|
// ============================================================================
|
package/src/utils.ts
CHANGED
|
@@ -133,6 +133,19 @@ export function escapeReplacement(str: string): string {
|
|
|
133
133
|
return str.replace(/\$/g, '$$$$')
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Path Resolution
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a source path to an absolute filesystem path.
|
|
142
|
+
* If the path is already absolute it is returned as-is; otherwise it is
|
|
143
|
+
* joined with the project root directory.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveSourcePath(sourcePath: string): string {
|
|
146
|
+
return path.isAbsolute(sourcePath) ? sourcePath : path.join(getProjectRoot(), sourcePath)
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
// ============================================================================
|
|
137
150
|
// Path Validation
|
|
138
151
|
// ============================================================================
|