reverse accepts new file paths via globs
All checks were successful
Publish npm package / publish (push) Successful in 22s
All checks were successful
Publish npm package / publish (push) Successful in 22s
This commit is contained in:
168
reverse.ts
168
reverse.ts
@@ -3,6 +3,7 @@ import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs"
|
||||
@@ -12,6 +13,7 @@ import type { ReverseMapFile, ReverseMapManifest, ReverseMapStoredTemplate, Reve
|
||||
|
||||
export type ReverseOptions = {
|
||||
mapPath?: string
|
||||
include?: string | string[]
|
||||
}
|
||||
|
||||
export type ReverseWarning = {
|
||||
@@ -57,6 +59,51 @@ function readManifest(mapPath: string): ReverseMapManifest {
|
||||
return manifest
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path.split("\\").join("/")
|
||||
}
|
||||
|
||||
function escapeRegExp(text: string): string {
|
||||
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
|
||||
}
|
||||
|
||||
function globToRegExp(glob: string): RegExp {
|
||||
let source = "^"
|
||||
const pattern = normalizePath(glob)
|
||||
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const char = pattern[i]!
|
||||
const next = pattern[i + 1]
|
||||
if (char === "*" && next === "*") {
|
||||
if (pattern[i + 2] === "/") {
|
||||
source += "(?:.*/)?"
|
||||
i += 2
|
||||
} else {
|
||||
source += ".*"
|
||||
i += 1
|
||||
}
|
||||
} else if (char === "*") {
|
||||
source += "[^/]*"
|
||||
} else if (char === "?") {
|
||||
source += "[^/]"
|
||||
} else {
|
||||
source += escapeRegExp(char)
|
||||
}
|
||||
}
|
||||
|
||||
return new RegExp(`${source}$`)
|
||||
}
|
||||
|
||||
function getIncludeMatchers(include: ReverseOptions["include"]): RegExp[] {
|
||||
if (!include) return []
|
||||
return (Array.isArray(include) ? include : [include]).map(globToRegExp)
|
||||
}
|
||||
|
||||
function matchesAny(path: string, matchers: RegExp[]): boolean {
|
||||
const normalized = normalizePath(path)
|
||||
return matchers.some(matcher => matcher.test(normalized))
|
||||
}
|
||||
|
||||
function replaceAtRange(content: string, token: ReverseMapToken): string | null {
|
||||
if (!token.range) return null
|
||||
const { start, end } = token.range
|
||||
@@ -198,6 +245,119 @@ function writeSkippedTemplate(
|
||||
return 1
|
||||
}
|
||||
|
||||
function dirnamePath(path: string): string {
|
||||
const normalized = normalizePath(path)
|
||||
const index = normalized.lastIndexOf("/")
|
||||
return index === -1 ? "" : normalized.slice(0, index)
|
||||
}
|
||||
|
||||
function basenamePath(path: string): string {
|
||||
const normalized = normalizePath(path)
|
||||
const index = normalized.lastIndexOf("/")
|
||||
return index === -1 ? normalized : normalized.slice(index + 1)
|
||||
}
|
||||
|
||||
function joinPath(...parts: string[]): string {
|
||||
return parts
|
||||
.filter(part => part !== "")
|
||||
.join("/")
|
||||
}
|
||||
|
||||
function buildDirectoryMap(manifest: ReverseMapManifest): Map<string, string> {
|
||||
const mappings = new Map<string, string>([["", ""]])
|
||||
|
||||
function addMapping(outputDir: string, templateDir: string) {
|
||||
const outputParts = normalizePath(outputDir).split("/").filter(Boolean)
|
||||
const templateParts = normalizePath(templateDir).split("/").filter(Boolean)
|
||||
for (let i = 1; i <= outputParts.length; i++) {
|
||||
if (i <= templateParts.length) {
|
||||
mappings.set(outputParts.slice(0, i).join("/"), templateParts.slice(0, i).join("/"))
|
||||
}
|
||||
}
|
||||
mappings.set(normalizePath(outputDir), normalizePath(templateDir))
|
||||
}
|
||||
|
||||
for (const file of manifest.files) {
|
||||
const output = normalizePath(file.outputPath)
|
||||
const template = normalizePath(file.templatePath)
|
||||
addMapping(dirnamePath(output), dirnamePath(template))
|
||||
if (file.tokens.some(token => token.kind === "path")) addMapping(output, template)
|
||||
}
|
||||
|
||||
for (const skipped of manifest.skipped ?? []) {
|
||||
if (skipped.kind === "directory") continue
|
||||
addMapping(dirnamePath(skipped.templatePath), dirnamePath(skipped.templatePath))
|
||||
}
|
||||
|
||||
return mappings
|
||||
}
|
||||
|
||||
function inferTemplatePath(outputPath: string, directoryMap: Map<string, string>): string {
|
||||
const normalized = normalizePath(outputPath)
|
||||
const outputDir = dirnamePath(normalized)
|
||||
let bestOutputDir = ""
|
||||
let bestTemplateDir = ""
|
||||
|
||||
for (const [mappedOutputDir, mappedTemplateDir] of directoryMap) {
|
||||
if (
|
||||
mappedOutputDir.length >= bestOutputDir.length
|
||||
&& (outputDir === mappedOutputDir || outputDir.startsWith(`${mappedOutputDir}/`))
|
||||
) {
|
||||
bestOutputDir = mappedOutputDir
|
||||
bestTemplateDir = mappedTemplateDir
|
||||
}
|
||||
}
|
||||
|
||||
const suffix = bestOutputDir === ""
|
||||
? outputDir
|
||||
: outputDir.slice(bestOutputDir.length).replace(/^\//, "")
|
||||
return joinPath(bestTemplateDir, suffix, basenamePath(normalized))
|
||||
}
|
||||
|
||||
function walkFiles(root: string, current = root): string[] {
|
||||
const files: string[] = []
|
||||
for (const entry of readdirSync(current).sort()) {
|
||||
const path = resolvePath(current, entry)
|
||||
const stat = statSync(path)
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...walkFiles(root, path))
|
||||
} else if (stat.isFile()) {
|
||||
files.push(normalizePath(relative(root, path)))
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function copyIncludedRenderedFiles(
|
||||
renderedRoot: string,
|
||||
templateRoot: string,
|
||||
mapPath: string,
|
||||
manifest: ReverseMapManifest,
|
||||
include: ReverseOptions["include"],
|
||||
): number {
|
||||
const matchers = getIncludeMatchers(include)
|
||||
if (matchers.length === 0) return 0
|
||||
|
||||
const mappedOutputPaths = new Set(manifest.files.map(file => normalizePath(file.outputPath)))
|
||||
const mapRelativePath = normalizePath(relative(renderedRoot, mapPath))
|
||||
const directoryMap = buildDirectoryMap(manifest)
|
||||
let filesWritten = 0
|
||||
|
||||
for (const outputPath of walkFiles(renderedRoot)) {
|
||||
if (outputPath === mapRelativePath) continue
|
||||
if (mappedOutputPaths.has(outputPath)) continue
|
||||
if (!matchesAny(outputPath, matchers)) continue
|
||||
|
||||
const renderedPath = resolveInside(renderedRoot, outputPath)
|
||||
const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap))
|
||||
mkdirSync(dirname(templatePath), { recursive: true })
|
||||
copyFileSync(renderedPath, templatePath)
|
||||
filesWritten += 1
|
||||
}
|
||||
|
||||
return filesWritten
|
||||
}
|
||||
|
||||
export function reverseDir(
|
||||
renderedDir: string,
|
||||
templateDir: string,
|
||||
@@ -252,5 +412,13 @@ export function reverseDir(
|
||||
filesWritten += writeSkippedTemplate(templateRoot, skipped)
|
||||
}
|
||||
|
||||
filesWritten += copyIncludedRenderedFiles(
|
||||
renderedRoot,
|
||||
templateRoot,
|
||||
mapPath,
|
||||
manifest,
|
||||
options.include,
|
||||
)
|
||||
|
||||
return { filesWritten, warnings }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user