|
@@ -13,6 +13,73 @@ import path from "path"
|
|
|
import process from "node:process"
|
|
import process from "node:process"
|
|
|
import { getLogger } from "./logging.js"
|
|
import { getLogger } from "./logging.js"
|
|
|
|
|
|
|
|
|
|
+function buildOutputPath(outDir, outputDir, pathsToStrip, inputPath, outputFileExtension) {
|
|
|
|
|
+ return path.join(
|
|
|
|
|
+ outDir,
|
|
|
|
|
+ outputDir || "",
|
|
|
|
|
+ replaceFileExtension(
|
|
|
|
|
+ removeBasePaths(pathsToStrip, inputPath),
|
|
|
|
|
+ outputFileExtension,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getPathsToStrip(config) {
|
|
|
|
|
+ return (config.stripPaths || []).map(p => expandTilde(p))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function paginateItems(items, itemsPerPage) {
|
|
|
|
|
+ if (!itemsPerPage || itemsPerPage <= 0 || !Array.isArray(items)) {
|
|
|
|
|
+ return [{ items, page: 1, totalPages: 1 }]
|
|
|
|
|
+ }
|
|
|
|
|
+ const totalPages = Math.ceil(items.length / itemsPerPage)
|
|
|
|
|
+ if (totalPages <= 1) {
|
|
|
|
|
+ return [{ items, page: 1, totalPages: 1 }]
|
|
|
|
|
+ }
|
|
|
|
|
+ const pages = []
|
|
|
|
|
+ for (let page = 1; page <= totalPages; page++) {
|
|
|
|
|
+ const start = (page - 1) * itemsPerPage
|
|
|
|
|
+ const end = start + itemsPerPage
|
|
|
|
|
+ pages.push({
|
|
|
|
|
+ items: items.slice(start, end),
|
|
|
|
|
+ page,
|
|
|
|
|
+ totalPages,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ return pages
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildPaginationMeta(page, totalPages, basePath) {
|
|
|
|
|
+ const ext = path.extname(basePath)
|
|
|
|
|
+ const base = basePath.slice(0, -ext.length)
|
|
|
|
|
+
|
|
|
|
|
+ const getPagePath = (pageNum) => {
|
|
|
|
|
+ if (pageNum === 1) return base.endsWith("/") ? base : `${base}/`
|
|
|
|
|
+ return `${base}/${pageNum}`
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pages = []
|
|
|
|
|
+ for (let i = 1; i <= totalPages; i++) {
|
|
|
|
|
+ pages.push({
|
|
|
|
|
+ pageNum: i,
|
|
|
|
|
+ path: getPagePath(i),
|
|
|
|
|
+ isCurrent: i === page,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ currentPage: page,
|
|
|
|
|
+ totalPages,
|
|
|
|
|
+ hasNextPage: page < totalPages,
|
|
|
|
|
+ hasPrevPage: page > 1,
|
|
|
|
|
+ nextPagePath: page < totalPages ? getPagePath(page + 1) : null,
|
|
|
|
|
+ prevPagePath: page > 1 ? getPagePath(page - 1) : null,
|
|
|
|
|
+ firstPagePath: getPagePath(1),
|
|
|
|
|
+ lastPagePath: getPagePath(totalPages),
|
|
|
|
|
+ pages,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export async function getConfig() {
|
|
export async function getConfig() {
|
|
|
const args = process.argv.slice(2)
|
|
const args = process.argv.slice(2)
|
|
|
const defaultPath = path.join(process.cwd(), "rhedyn.config.js")
|
|
const defaultPath = path.join(process.cwd(), "rhedyn.config.js")
|
|
@@ -38,8 +105,7 @@ export async function getConfig() {
|
|
|
const config = await import(configPath)
|
|
const config = await import(configPath)
|
|
|
return config.default || config
|
|
return config.default || config
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- console.error(`Error reading config file at ${configPath}:`, err)
|
|
|
|
|
- throw new Error("Failed reading config file")
|
|
|
|
|
|
|
+ throw new Error(`Failed reading config file at ${configPath}: ${err.message}`, { cause: err })
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
return
|
|
return
|
|
@@ -121,20 +187,18 @@ async function runTask({ meta, config, jobId }) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function selectFiles(patternsToInclude, config) {
|
|
function selectFiles(patternsToInclude, config) {
|
|
|
- const pathsToStrip = (config.stripPaths || []).map(path => expandTilde(path))
|
|
|
|
|
- const outputDir = config.outputDir || ""
|
|
|
|
|
|
|
+ const pathsToStrip = getPathsToStrip(config)
|
|
|
|
|
|
|
|
return async meta => {
|
|
return async meta => {
|
|
|
const filesToProcess = await readFilesByGlob(patternsToInclude)
|
|
const filesToProcess = await readFilesByGlob(patternsToInclude)
|
|
|
|
|
|
|
|
return filesToProcess.map(filePath => {
|
|
return filesToProcess.map(filePath => {
|
|
|
- const fileOutputPath = path.join(
|
|
|
|
|
|
|
+ const fileOutputPath = buildOutputPath(
|
|
|
meta.opts.outDir,
|
|
meta.opts.outDir,
|
|
|
- outputDir,
|
|
|
|
|
- replaceFileExtension(
|
|
|
|
|
- removeBasePaths(pathsToStrip, filePath),
|
|
|
|
|
- config.outputFileExtension,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ config.outputDir,
|
|
|
|
|
+ pathsToStrip,
|
|
|
|
|
+ filePath,
|
|
|
|
|
+ config.outputFileExtension,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
@@ -169,6 +233,9 @@ function selectState(stateToSelect, meta) {
|
|
|
return stateToSelect
|
|
return stateToSelect
|
|
|
.map(property => {
|
|
.map(property => {
|
|
|
const values = getValueAtPath(meta, property)
|
|
const values = getValueAtPath(meta, property)
|
|
|
|
|
+ if (values == null) {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
const expandedValues = Array.isArray(values)
|
|
const expandedValues = Array.isArray(values)
|
|
|
? values
|
|
? values
|
|
|
: Object.values(values)
|
|
: Object.values(values)
|
|
@@ -185,37 +252,73 @@ function selectState(stateToSelect, meta) {
|
|
|
|
|
|
|
|
async function expandStateTask(stateToExpand, config, meta) {
|
|
async function expandStateTask(stateToExpand, config, meta) {
|
|
|
const stateToProcess = selectState(stateToExpand, meta)
|
|
const stateToProcess = selectState(stateToExpand, meta)
|
|
|
- const pathsToStrip = (config.stripPaths || []).map(path => expandTilde(path))
|
|
|
|
|
- const outputDir = config.outputDir || ""
|
|
|
|
|
- return await Promise.all(
|
|
|
|
|
- stateToProcess.map(async (stateJob, index) => {
|
|
|
|
|
|
|
+ const pathsToStrip = getPathsToStrip(config)
|
|
|
|
|
+ const itemsPerPage = config.itemsPerPage ?? meta.opts.itemsPerPage
|
|
|
|
|
+
|
|
|
|
|
+ // Build jobs, potentially with pagination
|
|
|
|
|
+ const jobs = stateToProcess.flatMap((stateJob, index) => {
|
|
|
|
|
+ const items = Array.isArray(stateJob.value)
|
|
|
|
|
+ ? stateJob.value
|
|
|
|
|
+ : stateJob.value?.detail || []
|
|
|
|
|
+
|
|
|
|
|
+ // Only paginate if items is an array and itemsPerPage is set
|
|
|
|
|
+ const shouldPaginate = Array.isArray(items) && itemsPerPage > 0
|
|
|
|
|
+ const pages = shouldPaginate
|
|
|
|
|
+ ? paginateItems(items, itemsPerPage)
|
|
|
|
|
+ : [{ items, page: 1, totalPages: 1 }]
|
|
|
|
|
+
|
|
|
|
|
+ return pages.map(({ items: pageItems, page, totalPages }) => {
|
|
|
|
|
+ const basePath = buildOutputPath(
|
|
|
|
|
+ meta.opts.outDir,
|
|
|
|
|
+ config.outputDir,
|
|
|
|
|
+ pathsToStrip,
|
|
|
|
|
+ stateJob.key || String(index),
|
|
|
|
|
+ config.outputFileExtension,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // For page 2+, modify the output path
|
|
|
|
|
+ const fileOutputPath = page === 1
|
|
|
|
|
+ ? basePath
|
|
|
|
|
+ : (() => {
|
|
|
|
|
+ const ext = path.extname(basePath)
|
|
|
|
|
+ const base = basePath.slice(0, -ext.length)
|
|
|
|
|
+ return `${base}/${page}${ext}`
|
|
|
|
|
+ })()
|
|
|
|
|
+
|
|
|
|
|
+ const pagination = totalPages > 1
|
|
|
|
|
+ ? buildPaginationMeta(page, totalPages, basePath.replace(meta.opts.outDir, "/"))
|
|
|
|
|
+ : null
|
|
|
|
|
+
|
|
|
const decorations = {
|
|
const decorations = {
|
|
|
...(Array.isArray(stateJob.value)
|
|
...(Array.isArray(stateJob.value)
|
|
|
- ? { inputs: stateJob.value }
|
|
|
|
|
- : stateJob.value.detail),
|
|
|
|
|
-
|
|
|
|
|
- ...(config.buildFilePath
|
|
|
|
|
- ? {
|
|
|
|
|
- fileOutputPath: path.join(
|
|
|
|
|
- meta.opts.outDir,
|
|
|
|
|
- outputDir,
|
|
|
|
|
- replaceFileExtension(
|
|
|
|
|
- removeBasePaths(pathsToStrip, stateJob.key || String(index)),
|
|
|
|
|
- config.outputFileExtension,
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- }
|
|
|
|
|
- : {}),
|
|
|
|
|
|
|
+ ? { inputs: pageItems }
|
|
|
|
|
+ : { ...stateJob.value?.detail, inputs: pageItems }),
|
|
|
|
|
+ ...(config.buildFilePath ? { fileOutputPath } : {}),
|
|
|
|
|
+ ...(pagination ? { pagination } : {}),
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ stateJob,
|
|
|
|
|
+ index,
|
|
|
|
|
+ page,
|
|
|
|
|
+ decorations,
|
|
|
|
|
+ fileOutputPath,
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return await Promise.all(
|
|
|
|
|
+ jobs.map(async ({ stateJob, page, decorations }) => {
|
|
|
const jobConfig = {
|
|
const jobConfig = {
|
|
|
...config,
|
|
...config,
|
|
|
...decorations,
|
|
...decorations,
|
|
|
stateKey: stateJob.key,
|
|
stateKey: stateJob.key,
|
|
|
}
|
|
}
|
|
|
|
|
+ const pageInfo = page > 1 ? `:page-${page}` : ""
|
|
|
return runTask({
|
|
return runTask({
|
|
|
meta,
|
|
meta,
|
|
|
config: jobConfig,
|
|
config: jobConfig,
|
|
|
- jobId: `${config.name} @ ${stateJob.property}:${stateJob.index}`,
|
|
|
|
|
|
|
+ jobId: `${config.name} @ ${stateJob.property}:${stateJob.index}${pageInfo}`,
|
|
|
})
|
|
})
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
@@ -242,43 +345,103 @@ export async function expandAndRunTask(meta, config) {
|
|
|
|
|
|
|
|
if (config.stateSelectors) {
|
|
if (config.stateSelectors) {
|
|
|
if (config.expand === false) {
|
|
if (config.expand === false) {
|
|
|
- const pathsToStrip = (config.stripPaths || []).map(path =>
|
|
|
|
|
- expandTilde(path),
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const pathsToStrip = getPathsToStrip(config)
|
|
|
const inputs = selectState(config.stateSelectors, meta).map(stateItem => {
|
|
const inputs = selectState(config.stateSelectors, meta).map(stateItem => {
|
|
|
- return stateItem.value.detail
|
|
|
|
|
- ? stateItem.value.detail
|
|
|
|
|
- : stateItem.value
|
|
|
|
|
|
|
+ return stateItem.value?.detail ?? stateItem.value
|
|
|
})
|
|
})
|
|
|
- const decorations = {
|
|
|
|
|
- ...(config.buildFilePath
|
|
|
|
|
- ? {
|
|
|
|
|
- fileOutputPath: path.join(
|
|
|
|
|
- meta.opts.outDir,
|
|
|
|
|
- config.outputDir,
|
|
|
|
|
- replaceFileExtension(
|
|
|
|
|
- removeBasePaths(
|
|
|
|
|
- pathsToStrip,
|
|
|
|
|
- config.outputFileName || config.name,
|
|
|
|
|
- ),
|
|
|
|
|
- config.outputFileExtension,
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- }
|
|
|
|
|
- : {}),
|
|
|
|
|
|
|
+ const itemsPerPage = config.itemsPerPage
|
|
|
|
|
+ const shouldPaginate =
|
|
|
|
|
+ Array.isArray(inputs) && itemsPerPage != null && itemsPerPage > 0
|
|
|
|
|
+ const basePath =
|
|
|
|
|
+ (config.buildFilePath || shouldPaginate)
|
|
|
|
|
+ ? buildOutputPath(
|
|
|
|
|
+ meta.opts.outDir,
|
|
|
|
|
+ config.outputDir,
|
|
|
|
|
+ pathsToStrip,
|
|
|
|
|
+ config.outputFileName || config.name,
|
|
|
|
|
+ config.outputFileExtension,
|
|
|
|
|
+ )
|
|
|
|
|
+ : null
|
|
|
|
|
+
|
|
|
|
|
+ if (!shouldPaginate) {
|
|
|
|
|
+ const decorations = {
|
|
|
|
|
+ ...(config.buildFilePath
|
|
|
|
|
+ ? { fileOutputPath: basePath }
|
|
|
|
|
+ : {}),
|
|
|
|
|
+ }
|
|
|
|
|
+ const jobConfig = {
|
|
|
|
|
+ ...config,
|
|
|
|
|
+ ...decorations,
|
|
|
|
|
+ inputs,
|
|
|
|
|
+ }
|
|
|
|
|
+ const jobId = config.jobId || config.name
|
|
|
|
|
+ const taskResult = await runTask({
|
|
|
|
|
+ meta,
|
|
|
|
|
+ config: { ...jobConfig },
|
|
|
|
|
+ jobId,
|
|
|
|
|
+ })
|
|
|
|
|
+ return [taskResult]
|
|
|
}
|
|
}
|
|
|
- const jobConfig = {
|
|
|
|
|
- ...config,
|
|
|
|
|
- ...decorations,
|
|
|
|
|
- inputs,
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const pages = paginateItems(inputs, itemsPerPage)
|
|
|
|
|
+ if (pages.length <= 1) {
|
|
|
|
|
+ const decorations = {
|
|
|
|
|
+ ...(config.buildFilePath
|
|
|
|
|
+ ? { fileOutputPath: basePath }
|
|
|
|
|
+ : {}),
|
|
|
|
|
+ }
|
|
|
|
|
+ const jobConfig = {
|
|
|
|
|
+ ...config,
|
|
|
|
|
+ ...decorations,
|
|
|
|
|
+ inputs,
|
|
|
|
|
+ }
|
|
|
|
|
+ const jobId = config.jobId || config.name
|
|
|
|
|
+ const taskResult = await runTask({
|
|
|
|
|
+ meta,
|
|
|
|
|
+ config: { ...jobConfig },
|
|
|
|
|
+ jobId,
|
|
|
|
|
+ })
|
|
|
|
|
+ return [taskResult]
|
|
|
}
|
|
}
|
|
|
- const jobId = config.jobId || config.name
|
|
|
|
|
- const taskResult = await runTask({
|
|
|
|
|
- meta,
|
|
|
|
|
- config: { ...jobConfig },
|
|
|
|
|
- jobId,
|
|
|
|
|
- })
|
|
|
|
|
- return [taskResult]
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const basePathForPagination = basePath
|
|
|
|
|
+ ? basePath.replace(meta.opts.outDir, "/")
|
|
|
|
|
+ : null
|
|
|
|
|
+ const baseJobId = config.jobId || config.name
|
|
|
|
|
+ return await Promise.all(
|
|
|
|
|
+ pages.map(({ items: pageItems, page, totalPages }) => {
|
|
|
|
|
+ const fileOutputPath = basePath
|
|
|
|
|
+ ? (page === 1
|
|
|
|
|
+ ? basePath
|
|
|
|
|
+ : (() => {
|
|
|
|
|
+ const ext = path.extname(basePath)
|
|
|
|
|
+ const base = basePath.slice(0, -ext.length)
|
|
|
|
|
+ return `${base}/${page}${ext}`
|
|
|
|
|
+ })())
|
|
|
|
|
+ : undefined
|
|
|
|
|
+ const pagination =
|
|
|
|
|
+ totalPages > 1 && basePathForPagination
|
|
|
|
|
+ ? buildPaginationMeta(page, totalPages, basePathForPagination)
|
|
|
|
|
+ : null
|
|
|
|
|
+ const decorations = {
|
|
|
|
|
+ ...(config.buildFilePath && fileOutputPath
|
|
|
|
|
+ ? { fileOutputPath }
|
|
|
|
|
+ : {}),
|
|
|
|
|
+ ...(pagination ? { pagination } : {}),
|
|
|
|
|
+ }
|
|
|
|
|
+ const jobConfig = {
|
|
|
|
|
+ ...config,
|
|
|
|
|
+ ...decorations,
|
|
|
|
|
+ inputs: pageItems,
|
|
|
|
|
+ }
|
|
|
|
|
+ const pageInfo = page > 1 ? `:page-${page}` : ""
|
|
|
|
|
+ return runTask({
|
|
|
|
|
+ meta,
|
|
|
|
|
+ config: { ...jobConfig },
|
|
|
|
|
+ jobId: `${baseJobId}${pageInfo}`,
|
|
|
|
|
+ })
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return expandStateTask(config.stateSelectors, config, meta)
|
|
return expandStateTask(config.stateSelectors, config, meta)
|