Skip to content
15 changes: 15 additions & 0 deletions playground/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ const pages = defineCollection({
})

const collections = {
people: defineCollection({
type: 'data',
source: 'org/people.csv',
schema: z.object({
name: z.string(),
email: z.string().email(),
}),
}),
org: defineCollection({
type: 'data',
source: 'org/**.csv',
schema: z.object({
body: z.array(z.any()),
}),
}),
hackernews,
content,
data,
Expand Down
11 changes: 11 additions & 0 deletions playground/content/org/people.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name,email
John Doe,[email protected]
Jane Smith,[email protected]
Bob Johnson,[email protected]
Alice Brown,[email protected]
Charlie Wilson,[email protected]
Diana Lee,[email protected]
Eve Davis,[email protected]
Frank Miller,[email protected]
Grace Taylor,[email protected]
Henry Anderson,[email protected]
10 changes: 10 additions & 0 deletions playground/pages/org/data.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
const { data } = await useAsyncData('tmp-content', () => queryCollection('org').all())
</script>

<template>
<div>
<h1>People</h1>
<pre>{{ data }}</pre>
</div>
</template>
10 changes: 10 additions & 0 deletions playground/pages/org/people.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
const { data: tmpContent } = await useAsyncData('tmp-content', () => queryCollection('people').all())
</script>

<template>
<div>
<h1>People</h1>
<pre>{{ tmpContent }}</pre>
</div>
</template>
34 changes: 5 additions & 29 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,42 +327,18 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
body: content,
path: fullPath,
})
if (parsedContent) {
db.insertDevelopmentCache(keyInCollection, JSON.stringify(parsedContent), checksum)
}
}

// Add manually provided components from the content
if (parsedContent?.__metadata?.components) {
usedComponents.push(...parsedContent.__metadata.components)
}

// Special handling for CSV files
if (parsedContent.extension === 'csv') {
const rows = (parsedContent.meta as { path: string, body: Array<Record<string, string>> })?.body
if (rows && Array.isArray(rows)) {
// Since csv files can contain multiple rows, we can't process it as a single ParsedContent
// As for id, priority: `id` field > first column > index
for (let i = 0; i < rows.length; i++) {
if (!rows[i]) {
logger.warn(`"${keyInCollection}" row ${i} is undefined and will be ignored.`)
continue
}
const rowid = rows[i]?.id || Object.values(rows[i] || {})?.at(0) || String(i)
const rowContent = {
id: parsedContent.id + '/' + rowid,
...rows[i],
}
db.insertDevelopmentCache(parsedContent.id + '/' + rowid, JSON.stringify(rowContent), checksum)
const { queries, hash } = generateCollectionInsert(collection, rowContent)
list.push([key, queries, hash])
}
}
}
else {
if (parsedContent) {
db.insertDevelopmentCache(keyInCollection, JSON.stringify(parsedContent), checksum)
}
const { queries, hash } = generateCollectionInsert(collection, parsedContent)
list.push([key, queries, hash])
}
const { queries, hash } = generateCollectionInsert(collection, parsedContent)
Copy link
Author

@Jasonzyt Jasonzyt Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure why you reverted this.
Since CSV files contain multiple rows, and each row should be treated as an independent ParsedContent, they require a special handling process. Each CSV file should not be processed as a single ParsedContent.

In contrast, formats like Markdown/JSON/YML contain only one ParsedContent per file, so they must be handled differently.

And the facts prove this point: after installing the latest package, errors occurred.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSV and other formats must be handled seperately. Maybe we can find a better way to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to do this, Content now has defineCSVSource which reads csv file and generate a document for each row

list.push([key, queries, hash])
}
catch (e: unknown) {
logger.warn(`"${keyInCollection}" is ignored because parsing is failed. Error: ${e instanceof Error ? e.message : 'Unknown error'}`)
Expand Down
7 changes: 7 additions & 0 deletions src/utils/content/transformers/csv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export default defineTransformer({
})
const { result } = await stream.process(file.body)

if (Array.isArray(result) && result.length === 1) {
return {
id: file.id,
...result[0],
}
}

return {
id: file.id,
body: result,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function detectSchemaVendor(schema: ContentStandardSchemaV1) {
}

export function replaceComponentSchemas<T = Draft07Definition | Draft07DefinitionProperty>(property: T): T {
if ((property as Draft07DefinitionProperty).type === 'array') {
if ((property as Draft07DefinitionProperty).type === 'array' && (property as Draft07DefinitionProperty).items) {
(property as Draft07DefinitionProperty).items = replaceComponentSchemas((property as Draft07DefinitionProperty).items as Draft07DefinitionProperty) as Draft07DefinitionProperty
}

Expand Down
59 changes: 59 additions & 0 deletions src/utils/source.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { readFile } from 'node:fs/promises'
import { createReadStream } from 'node:fs'
import { join, normalize } from 'pathe'
import { withLeadingSlash, withoutTrailingSlash } from 'ufo'
import { glob } from 'tinyglobby'
Expand All @@ -19,6 +20,12 @@ export function defineLocalSource(source: CollectionSource | ResolvedCollectionS
logger.warn('Collection source should not start with `./` or `../`.')
source.include = source.include.replace(/^(\.\/|\.\.\/|\/)*/, '')
}

// If source is a CSV file, define a CSV source
if (source.include.endsWith('.csv') && !source.include.includes('*')) {
return defineCSVSource(source)
}

const { fixed } = parseSourceBase(source)
const resolvedSource: ResolvedCollectionSource = {
_resolved: true,
Expand Down Expand Up @@ -105,6 +112,58 @@ export function defineBitbucketSource(
return resolvedSource
}

export function defineCSVSource(source: CollectionSource): ResolvedCollectionSource {
const { fixed } = parseSourceBase(source)

const resolvedSource: ResolvedCollectionSource = {
_resolved: true,
prefix: withoutTrailingSlash(withLeadingSlash(fixed)),
prepare: async ({ rootDir }) => {
resolvedSource.cwd = source.cwd
? String(normalize(source.cwd)).replace(/^~~\//, rootDir)
: join(rootDir, 'content')
},
getKeys: async () => {
const _keys = await glob(source.include, { cwd: resolvedSource.cwd, ignore: getExcludedSourcePaths(source), dot: true, expandDirectories: false })
.catch((): [] => [])
const keys = _keys.map(key => key.substring(fixed.length))
if (keys.length !== 1) {
return keys
}

return new Promise((resolve) => {
const csvKeys: string[] = []
let count = 0
createReadStream(join(resolvedSource.cwd, fixed, keys[0]!))
.on('data', function (chunk) {
for (let i = 0; i < chunk.length; i += 1)
if (chunk[i] == 10) {
csvKeys.push(`${keys[0]}#l${count}`)
count += 1
}
})
.on('end', () => resolve(csvKeys))
})
},
getItem: async (key) => {
const [csvKey, csvIndex] = key.split('#')
const fullPath = join(resolvedSource.cwd, fixed, csvKey!)
const content = await readFile(fullPath, 'utf8')

if (key.includes('#')) {
const lines = content.split('\n')
return lines[0] + '\n' + lines[+(csvIndex || 0)]!
}

return content
},
...source,
include: source.include,
cwd: '',
}
return resolvedSource
}

export function parseSourceBase(source: CollectionSource) {
const [fixPart, ...rest] = source.include.includes('*') ? source.include.split('*') : ['', source.include]
return {
Expand Down
Loading