From 9f22ac3d097ef2cb3b2bbe5343b8a8a49d83425d Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 3 Feb 2023 16:48:34 -0500 Subject: [PATCH] [Content collections] Fix "underscore to ignore" warnings (#6122) * refactor: fix underscore check * fix: add "ignore list" to always silence log * fix: hide log on file deleted * refactor: move getEntryType to util * test: getEntryType unit * fix: handle all unsupported cases * chore: changeset --- .changeset/grumpy-bees-worry.md | 5 ++ packages/astro/src/content/types-generator.ts | 45 +++++--------- packages/astro/src/content/utils.ts | 37 ++++++++++- .../content/vite-plugin-content-imports.ts | 2 +- .../get-entry-type.test.js | 62 +++++++++++++++++++ 5 files changed, 117 insertions(+), 34 deletions(-) create mode 100644 .changeset/grumpy-bees-worry.md create mode 100644 packages/astro/test/units/content-collections/get-entry-type.test.js diff --git a/.changeset/grumpy-bees-worry.md b/.changeset/grumpy-bees-worry.md new file mode 100644 index 000000000000..bab88ad25eba --- /dev/null +++ b/.changeset/grumpy-bees-worry.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Content collections: Fix accidental "use underscore to ignore" logs for `.DS_Store` files and underscored directory names. diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index da0e84dcc23f..50f8088ded2c 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -6,8 +6,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { normalizePath, ViteDevServer } from 'vite'; import type { AstroSettings } from '../@types/astro.js'; import { info, LogOptions, warn } from '../core/logger/core.js'; -import { appendForwardSlash, isRelativePath } from '../core/path.js'; -import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js'; +import { isRelativePath } from '../core/path.js'; +import { CONTENT_TYPES_FILE } from './consts.js'; import { ContentConfig, ContentObservable, @@ -16,6 +16,7 @@ import { getContentPaths, getEntryInfo, getEntrySlug, + getEntryType, loadContentConfig, NoCollectionError, parseFrontmatter, @@ -111,7 +112,7 @@ export async function createContentTypesGenerator({ return { shouldGenerateTypes: true }; } const fileType = getEntryType(fileURLToPath(event.entry), contentPaths); - if (fileType === 'generated-types') { + if (fileType === 'ignored') { return { shouldGenerateTypes: false }; } if (fileType === 'config') { @@ -125,22 +126,21 @@ export async function createContentTypesGenerator({ return { shouldGenerateTypes: true }; } - if (fileType === 'unknown') { + if (fileType === 'unsupported') { + // Avoid warning if file was deleted. + if (event.name === 'unlink') { + return { shouldGenerateTypes: false }; + } const entryInfo = getEntryInfo({ entry: event.entry, contentDir: contentPaths.contentDir, - // Allow underscore `_` files outside collection directories + // Skip invalid file check. We already know it’s invalid. allowFilesOutsideCollection: true, }); - if (entryInfo.id.startsWith('_') && (event.name === 'add' || event.name === 'change')) { - // Silently ignore `_` files. - return { shouldGenerateTypes: false }; - } else { - return { - shouldGenerateTypes: false, - error: new UnsupportedFileTypeError(entryInfo.id), - }; - } + return { + shouldGenerateTypes: false, + error: new UnsupportedFileTypeError(entryInfo.id), + }; } const entryInfo = getEntryInfo({ entry: event.entry, @@ -289,23 +289,6 @@ function removeEntry(contentTypes: ContentTypes, collectionKey: string, entryKey delete contentTypes[collectionKey][entryKey]; } -export function getEntryType( - entryPath: string, - paths: ContentPaths -): 'content' | 'config' | 'unknown' | 'generated-types' { - const { dir: rawDir, ext, base } = path.parse(entryPath); - const dir = appendForwardSlash(pathToFileURL(rawDir).href); - if ((contentFileExts as readonly string[]).includes(ext)) { - return 'content'; - } else if (new URL(base, dir).href === paths.config.href) { - return 'config'; - } else if (new URL(base, dir).href === new URL(CONTENT_TYPES_FILE, paths.cacheDir).href) { - return 'generated-types'; - } else { - return 'unknown'; - } -} - async function writeContentFiles({ fs, contentPaths, diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index a2c4631896dc..a1ddba757037 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -2,12 +2,13 @@ import { slug as githubSlug } from 'github-slugger'; import matter from 'gray-matter'; import type fsMod from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from 'vite'; import { z } from 'zod'; import { AstroConfig, AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; -import { CONTENT_TYPES_FILE } from './consts.js'; +import { appendForwardSlash } from '../core/path.js'; +import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js'; export const collectionConfigParser = z.object({ schema: z.any().optional(), @@ -158,6 +159,38 @@ export function getEntryInfo({ return res; } +export function getEntryType( + entryPath: string, + paths: Pick +): 'content' | 'config' | 'ignored' | 'unsupported' { + const { dir: rawDir, ext, base } = path.parse(entryPath); + const dir = appendForwardSlash(pathToFileURL(rawDir).href); + const fileUrl = new URL(base, dir); + + if (hasUnderscoreInPath(fileUrl) || isOnIgnoreList(fileUrl)) { + return 'ignored'; + } else if ((contentFileExts as readonly string[]).includes(ext)) { + return 'content'; + } else if (fileUrl.href === paths.config.href) { + return 'config'; + } else { + return 'unsupported'; + } +} + +function isOnIgnoreList(fileUrl: URL) { + const { base } = path.parse(fileURLToPath(fileUrl)); + return ['.DS_Store'].includes(base); +} + +function hasUnderscoreInPath(fileUrl: URL): boolean { + const parts = fileUrl.pathname.split('/'); + for (const part of parts) { + if (part.startsWith('_')) return true; + } + return false; +} + const flattenErrorPath = (errorPath: (string | number)[]) => errorPath.join('.'); const errorMap: z.ZodErrorMap = (error, ctx) => { diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 66d20d7031ae..7c7e8f65c6c7 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -7,7 +7,7 @@ import { AstroErrorData } from '../core/errors/errors-data.js'; import { AstroError } from '../core/errors/errors.js'; import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js'; import { contentFileExts, CONTENT_FLAG } from './consts.js'; -import { getEntryType } from './types-generator.js'; +import { getEntryType } from './utils.js'; import { ContentConfig, getContentPaths, diff --git a/packages/astro/test/units/content-collections/get-entry-type.test.js b/packages/astro/test/units/content-collections/get-entry-type.test.js new file mode 100644 index 000000000000..3248a88f617e --- /dev/null +++ b/packages/astro/test/units/content-collections/get-entry-type.test.js @@ -0,0 +1,62 @@ +import { getEntryType } from '../../../dist/content/utils.js'; +import { expect } from 'chai'; +import { fileURLToPath } from 'node:url'; + +describe('Content Collections - getEntryType', () => { + const contentDir = new URL('src/content/', import.meta.url); + const contentPaths = { + config: new URL('src/content/config.ts', import.meta.url), + }; + + it('Returns "content" for Markdown files', () => { + for (const entryPath of ['blog/first-post.md', 'blog/first-post.mdx']) { + const entry = fileURLToPath(new URL(entryPath, contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('content'); + } + }); + + it('Returns "content" for Markdown files in nested directories', () => { + for (const entryPath of ['blog/2021/01/01/index.md', 'blog/2021/01/01/index.mdx']) { + const entry = fileURLToPath(new URL(entryPath, contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('content'); + } + }); + + it('Returns "config" for config files', () => { + const entry = fileURLToPath(contentPaths.config); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('config'); + }); + + it('Returns "unsupported" for non-Markdown files', () => { + const entry = fileURLToPath(new URL('blog/robots.txt', contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('unsupported'); + }); + + it('Returns "ignored" for .DS_Store', () => { + const entry = fileURLToPath(new URL('blog/.DS_Store', contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('ignored'); + }); + + it('Returns "ignored" for unsupported files using an underscore', () => { + const entry = fileURLToPath(new URL('blog/_draft-robots.txt', contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('ignored'); + }); + + it('Returns "ignored" when using underscore on file name', () => { + const entry = fileURLToPath(new URL('blog/_first-post.md', contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('ignored'); + }); + + it('Returns "ignored" when using underscore on directory name', () => { + const entry = fileURLToPath(new URL('blog/_draft/first-post.md', contentDir)); + const type = getEntryType(entry, contentPaths); + expect(type).to.equal('ignored'); + }); +});