From 9d092b56c74e1e0c425782f271c6713832ca394e Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Fri, 14 May 2021 11:03:41 -0500 Subject: [PATCH] Fix Windows tests (#212) * Fix Windows fetchContent() * Fix Windows bundling & config loading * Fix astro-prettier formatting for Windows --- .changeset/itchy-carrots-visit.md | 5 + packages/astro/package.json | 7 +- packages/astro/src/build.ts | 275 ++++++++---------- packages/astro/src/build/page.ts | 48 +-- packages/astro/src/build/rss.ts | 32 +- packages/astro/src/build/util.ts | 13 +- .../astro/src/compiler/codegen/content.ts | 32 +- packages/astro/src/compiler/codegen/index.ts | 19 +- packages/astro/src/config.ts | 12 +- packages/astro/src/search.ts | 9 +- packages/astro/test/astro-collection.test.js | 21 +- packages/astro/test/astro-markdown.test.js | 54 +--- packages/astro/test/astro-rss.test.js | 4 +- packages/astro/test/config-port.test.js | 4 +- .../astro-collection/src/pages/$nested.astro | 27 ++ .../src/pages/$paginated.astro | 28 ++ .../pages/{$posts.astro => $shallow.astro} | 2 +- .../test/astro-prettier.test.js | 2 +- yarn.lock | 13 +- 19 files changed, 307 insertions(+), 300 deletions(-) create mode 100644 .changeset/itchy-carrots-visit.md create mode 100644 packages/astro/test/fixtures/astro-collection/src/pages/$nested.astro create mode 100644 packages/astro/test/fixtures/astro-collection/src/pages/$paginated.astro rename packages/astro/test/fixtures/astro-collection/src/pages/{$posts.astro => $shallow.astro} (97%) diff --git a/.changeset/itchy-carrots-visit.md b/.changeset/itchy-carrots-visit.md new file mode 100644 index 000000000..9870e263b --- /dev/null +++ b/.changeset/itchy-carrots-visit.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Bugfix: Windows collection API path bug diff --git a/packages/astro/package.json b/packages/astro/package.json index b0d1285fc..8328350a7 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -76,12 +76,14 @@ "rehype-parse": "^7.0.1", "rollup": "^2.43.1", "rollup-plugin-terser": "^7.0.2", - "sass": "^1.32.8", + "sass": "^1.32.13", "shorthash": "^0.0.2", + "slash": "^4.0.0", "snowpack": "^3.3.7", "source-map-support": "^0.5.19", "string-width": "^5.0.0", "svelte": "^3.35.0", + "tiny-glob": "^0.2.8", "unified": "^9.2.1", "vue": "^3.0.10", "yargs-parser": "^20.2.7" @@ -100,8 +102,7 @@ "@types/react-dom": "^17.0.2", "@types/sass": "^1.16.0", "@types/yargs-parser": "^20.2.0", - "astro-scripts": "0.0.1", - "slash": "^4.0.0" + "astro-scripts": "0.0.1" }, "engines": { "node": ">=14.0.0", diff --git a/packages/astro/src/build.ts b/packages/astro/src/build.ts index 439d4ac9b..783f0ec06 100644 --- a/packages/astro/src/build.ts +++ b/packages/astro/src/build.ts @@ -10,7 +10,7 @@ import cheerio from 'cheerio'; import del from 'del'; import { bold, green, yellow } from 'kleur/colors'; import mime from 'mime'; -import { fdir } from 'fdir'; +import glob from 'tiny-glob'; import { bundleCSS } from './build/bundle/css.js'; import { bundleJS, collectJSImports } from './build/bundle/js'; import { buildCollectionPage, buildStaticPage, getPageType } from './build/page.js'; @@ -26,13 +26,10 @@ const logging: LogOptions = { }; /** Return contents of src/pages */ -async function allPages(root: URL) { - const api = new fdir() - .filter((p) => /\.(astro|md)$/.test(p)) - .withFullPaths() - .crawl(fileURLToPath(root)); - const files = await api.withPromise(); - return files as string[]; +async function allPages(root: URL): Promise { + const cwd = fileURLToPath(root); + const files = await glob('**/*.{astro,md}', { cwd, filesOnly: true }); + return files.map((f) => new URL(f, root)); } /** Is this URL remote? */ @@ -63,22 +60,20 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const { runtimeConfig } = runtime; const { backendSnowpack: snowpack } = runtimeConfig; - const pages = await allPages(pageRoot); - - // 0. erase build directory - await del(fileURLToPath(dist)); - - /** - * 1. Build Pages - * Source files are built in parallel and stored in memory. Most assets are also gathered here, too. - */ - timer.build = performance.now(); try { + // 0. erase build directory + await del(fileURLToPath(dist)); + + /** + * 1. Build Pages + * Source files are built in parallel and stored in memory. Most assets are also gathered here, too. + */ + timer.build = performance.now(); + const pages = await allPages(pageRoot); info(logging, 'build', yellow('! building pages...')); const release = trapWarn(); // Vue also console.warns, this silences it. await Promise.all( - pages.map(async (pathname) => { - const filepath = new URL(`file://${pathname}`); + pages.map(async (filepath) => { const buildPage = getPageType(filepath) === 'collection' ? buildCollectionPage : buildStaticPage; await buildPage({ astroConfig, @@ -94,152 +89,140 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { ); info(logging, 'build', green('✔'), 'pages built.'); release(); - } catch (err) { - error(logging, 'generate', err); - await runtime.shutdown(); - return 1; - } - debug(logging, 'build', `built pages [${stopTimer(timer.build)}]`); + debug(logging, 'build', `built pages [${stopTimer(timer.build)}]`); - // after pages are built, build depTree - timer.deps = performance.now(); - const scanPromises: Promise[] = []; - for (const id of Object.keys(buildState)) { - if (buildState[id].contentType !== 'text/html') continue; // only scan HTML files - const pageDeps = findDeps(buildState[id].contents as string, { - astroConfig, - srcPath: buildState[id].srcPath, - }); - depTree[id] = pageDeps; + // after pages are built, build depTree + timer.deps = performance.now(); + const scanPromises: Promise[] = []; + for (const id of Object.keys(buildState)) { + if (buildState[id].contentType !== 'text/html') continue; // only scan HTML files + const pageDeps = findDeps(buildState[id].contents as string, { + astroConfig, + srcPath: buildState[id].srcPath, + }); + depTree[id] = pageDeps; - // while scanning we will find some unbuilt files; make sure those are all built while scanning - for (const url of [...pageDeps.js, ...pageDeps.css, ...pageDeps.images]) { - if (!buildState[url]) - scanPromises.push( - runtime.load(url).then((result) => { - if (result.statusCode !== 200) { - // there shouldn’t be a build error here - throw (result as any).error || new Error(`unexpected status ${result.statusCode} when loading ${url}`); - } - buildState[url] = { - srcPath: new URL(url, projectRoot), - contents: result.contents, - contentType: result.contentType || mime.getType(url) || '', - }; - }) - ); + // while scanning we will find some unbuilt files; make sure those are all built while scanning + for (const url of [...pageDeps.js, ...pageDeps.css, ...pageDeps.images]) { + if (!buildState[url]) + scanPromises.push( + runtime.load(url).then((result) => { + if (result.statusCode !== 200) { + // there shouldn’t be a build error here + throw (result as any).error || new Error(`unexpected status ${result.statusCode} when loading ${url}`); + } + buildState[url] = { + srcPath: new URL(url, projectRoot), + contents: result.contents, + contentType: result.contentType || mime.getType(url) || '', + }; + }) + ); + } } - } - try { await Promise.all(scanPromises); - } catch (err) { - error(logging, 'build', err); - return 1; - } - debug(logging, 'build', `scanned deps [${stopTimer(timer.deps)}]`); + debug(logging, 'build', `scanned deps [${stopTimer(timer.deps)}]`); - /** - * 2. Bundling 1st Pass: In-memory - * Bundle CSS, and anything else that can happen in memory (for now, JS bundling happens after writing to disk) - */ - info(logging, 'build', yellow('! optimizing css...')); - timer.prebundle = performance.now(); - await Promise.all([ - bundleCSS({ buildState, astroConfig, logging, depTree }).then(() => { - debug(logging, 'build', `bundled CSS [${stopTimer(timer.prebundle)}]`); - }), - // TODO: optimize images? - ]); - // TODO: minify HTML? - info(logging, 'build', green('✔'), 'css optimized.'); + /** + * 2. Bundling 1st Pass: In-memory + * Bundle CSS, and anything else that can happen in memory (for now, JS bundling happens after writing to disk) + */ + info(logging, 'build', yellow('! optimizing css...')); + timer.prebundle = performance.now(); + await Promise.all([ + bundleCSS({ buildState, astroConfig, logging, depTree }).then(() => { + debug(logging, 'build', `bundled CSS [${stopTimer(timer.prebundle)}]`); + }), + // TODO: optimize images? + ]); + // TODO: minify HTML? + info(logging, 'build', green('✔'), 'css optimized.'); - /** - * 3. Write to disk - * Also clear in-memory bundle - */ - // collect stats output - const urlStats = await collectBundleStats(buildState, depTree); + /** + * 3. Write to disk + * Also clear in-memory bundle + */ + // collect stats output + const urlStats = await collectBundleStats(buildState, depTree); - // collect JS imports for bundling - const jsImports = await collectJSImports(buildState); + // collect JS imports for bundling + const jsImports = await collectJSImports(buildState); - // write sitemap - if (astroConfig.buildOptions.sitemap && astroConfig.buildOptions.site) { - timer.sitemap = performance.now(); - info(logging, 'build', yellow('! creating sitemap...')); - const sitemap = generateSitemap(buildState, astroConfig.buildOptions.site); - const sitemapPath = new URL('sitemap.xml', dist); - await fs.promises.mkdir(path.dirname(fileURLToPath(sitemapPath)), { recursive: true }); - await fs.promises.writeFile(sitemapPath, sitemap, 'utf8'); - info(logging, 'build', green('✔'), 'sitemap built.'); - debug(logging, 'build', `built sitemap [${stopTimer(timer.sitemap)}]`); - } + // write sitemap + if (astroConfig.buildOptions.sitemap && astroConfig.buildOptions.site) { + timer.sitemap = performance.now(); + info(logging, 'build', yellow('! creating sitemap...')); + const sitemap = generateSitemap(buildState, astroConfig.buildOptions.site); + const sitemapPath = new URL('sitemap.xml', dist); + await fs.promises.mkdir(path.dirname(fileURLToPath(sitemapPath)), { recursive: true }); + await fs.promises.writeFile(sitemapPath, sitemap, 'utf8'); + info(logging, 'build', green('✔'), 'sitemap built.'); + debug(logging, 'build', `built sitemap [${stopTimer(timer.sitemap)}]`); + } - // write to disk and free up memory - timer.write = performance.now(); - await Promise.all( - Object.keys(buildState).map(async (id) => { - const outPath = new URL(`.${id}`, dist); - const parentDir = path.posix.dirname(fileURLToPath(outPath)); - await fs.promises.mkdir(parentDir, { recursive: true }); - await fs.promises.writeFile(outPath, buildState[id].contents, buildState[id].encoding); - delete buildState[id]; - delete depTree[id]; - }) - ); - debug(logging, 'build', `wrote files to disk [${stopTimer(timer.write)}]`); - - /** - * 4. Copy Public Assets - */ - if (fs.existsSync(astroConfig.public)) { - info(logging, 'build', yellow(`! copying public folder...`)); - timer.public = performance.now(); - const pub = astroConfig.public; - const publicFiles = (await new fdir().withFullPaths().crawl(fileURLToPath(pub)).withPromise()) as string[]; + // write to disk and free up memory + timer.write = performance.now(); await Promise.all( - publicFiles.map(async (filepath) => { - const fileUrl = new URL(`file://${filepath}`); - const rel = path.relative(fileURLToPath(pub), fileURLToPath(fileUrl)); - const outPath = new URL('./' + rel, dist); - await fs.promises.mkdir(path.dirname(fileURLToPath(outPath)), { recursive: true }); - await fs.promises.copyFile(fileUrl, outPath); + Object.keys(buildState).map(async (id) => { + const outPath = new URL(`.${id}`, dist); + const parentDir = path.dirname(fileURLToPath(outPath)); + await fs.promises.mkdir(parentDir, { recursive: true }); + await fs.promises.writeFile(outPath, buildState[id].contents, buildState[id].encoding); + delete buildState[id]; + delete depTree[id]; }) ); - debug(logging, 'build', `copied public folder [${stopTimer(timer.public)}]`); - info(logging, 'build', green('✔'), 'public folder copied.'); - } else { - if (path.basename(astroConfig.public.toString()) !== 'public') { - info(logging, 'tip', yellow(`! no public folder ${astroConfig.public} found...`)); - } - } + debug(logging, 'build', `wrote files to disk [${stopTimer(timer.write)}]`); - /** - * 5. Bundling 2nd Pass: On disk - * Bundle JS, which requires hard files to optimize - */ - info(logging, 'build', yellow(`! bundling...`)); - if (jsImports.size > 0) { - try { + /** + * 4. Copy Public Assets + */ + if (fs.existsSync(astroConfig.public)) { + info(logging, 'build', yellow(`! copying public folder...`)); + timer.public = performance.now(); + const cwd = fileURLToPath(astroConfig.public); + const publicFiles = await glob('**/*', { cwd, filesOnly: true }); + await Promise.all( + publicFiles.map(async (filepath) => { + const srcPath = new URL(filepath, astroConfig.public); + const distPath = new URL(filepath, dist); + await fs.promises.mkdir(path.dirname(fileURLToPath(distPath)), { recursive: true }); + await fs.promises.copyFile(srcPath, distPath); + }) + ); + debug(logging, 'build', `copied public folder [${stopTimer(timer.public)}]`); + info(logging, 'build', green('✔'), 'public folder copied.'); + } else { + if (path.basename(astroConfig.public.toString()) !== 'public') { + info(logging, 'tip', yellow(`! no public folder ${astroConfig.public} found...`)); + } + } + + /** + * 5. Bundling 2nd Pass: On disk + * Bundle JS, which requires hard files to optimize + */ + info(logging, 'build', yellow(`! bundling...`)); + if (jsImports.size > 0) { timer.bundleJS = performance.now(); const jsStats = await bundleJS(jsImports, { dist: new URL(dist + '/', projectRoot), runtime }); mapBundleStatsToURLStats({ urlStats, depTree, bundleStats: jsStats }); debug(logging, 'build', `bundled JS [${stopTimer(timer.bundleJS)}]`); info(logging, 'build', green(`✔`), 'bundling complete.'); - } catch (err) { - error(logging, 'build', err); - await runtime.shutdown(); - return 1; } - } - /** - * 6. Print stats - */ - logURLStats(logging, urlStats); - await runtime.shutdown(); - info(logging, 'build', bold(green('▶ Build Complete!'))); - return 0; + /** + * 6. Print stats + */ + logURLStats(logging, urlStats); + await runtime.shutdown(); + info(logging, 'build', bold(green('▶ Build Complete!'))); + return 0; + } catch (err) { + error(logging, 'build', err); + await runtime.shutdown(); + return 1; + } } /** Given an HTML string, collect and tags */ diff --git a/packages/astro/src/build/page.ts b/packages/astro/src/build/page.ts index 1f0093271..cc28040d6 100644 --- a/packages/astro/src/build/page.ts +++ b/packages/astro/src/build/page.ts @@ -37,9 +37,10 @@ export function getPageType(filepath: URL): 'collection' | 'static' { /** Build collection */ export async function buildCollectionPage({ astroConfig, filepath, logging, mode, runtime, site, resolvePackageUrl, buildState }: PageBuildOptions): Promise { - const rel = path.posix.relative(fileURLToPath(astroConfig.astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro - const pagePath = `/${rel.replace(/\$([^.]+)\.astro$/, '$1')}`; - const srcPath = new URL('pages/' + rel, astroConfig.astroRoot); + const pagesPath = new URL('./pages/', astroConfig.astroRoot); + const srcURL = filepath.pathname.replace(pagesPath.pathname, '/'); + const outURL = srcURL.replace(/\$([^.]+)\.astro$/, '$1'); + const builtURLs = new Set(); // !important: internal cache that prevents building the same URLs /** Recursively build collection URLs */ @@ -48,9 +49,9 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode const result = await runtime.load(url); builtURLs.add(url); if (result.statusCode === 200) { - const outPath = path.posix.join('/', url, 'index.html'); + const outPath = path.posix.join(url, '/index.html'); buildState[outPath] = { - srcPath, + srcPath: filepath, contents: result.contents, contentType: 'text/html', encoding: 'utf8', @@ -60,7 +61,7 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode } const [result] = await Promise.all([ - loadCollection(pagePath) as Promise, // first run will always return a result so assert type here + loadCollection(outURL) as Promise, // first run will always return a result so assert type here gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }), ]); @@ -68,7 +69,7 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode throw new Error((result as any).error); } if (result.statusCode === 200 && !result.collectionInfo) { - throw new Error(`[${rel}]: Collection page must export createCollection() function`); + throw new Error(`[${srcURL}]: Collection page must export createCollection() function`); } // note: for pages that require params (/tag/:tag), we will get a 404 but will still get back collectionInfo that tell us what the URLs should be @@ -87,11 +88,12 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode ); if (result.collectionInfo.rss) { - if (!site) throw new Error(`[${rel}] createCollection() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`); - const rss = generateRSS({ ...(result.collectionInfo.rss as any), site }, rel.replace(/\$([^.]+)\.astro$/, '$1')); - const feedURL = path.posix.join('/feed', `${pagePath}.xml`); + if (!site) throw new Error(`[${srcURL}] createCollection() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`); + let feedURL = outURL === '/' ? '/index' : outURL; + feedURL = '/feed' + feedURL + '.xml'; + const rss = generateRSS({ ...(result.collectionInfo.rss as any), site }, { srcFile: srcURL, feedURL }); buildState[feedURL] = { - srcPath, + srcPath: filepath, contents: rss, contentType: 'application/rss+xml', encoding: 'utf8', @@ -102,22 +104,20 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode /** Build static page */ export async function buildStaticPage({ astroConfig, buildState, filepath, logging, mode, resolvePackageUrl, runtime }: PageBuildOptions): Promise { - const rel = path.posix.relative(fileURLToPath(astroConfig.astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro - const pagePath = `/${rel.replace(/\.(astro|md)$/, '')}`; - - let relPath = path.posix.join('/', rel.replace(/\.(astro|md)$/, '.html')); - if (!relPath.endsWith('index.html')) { - relPath = relPath.replace(/\.html$/, '/index.html'); - } - - const srcPath = new URL('pages/' + rel, astroConfig.astroRoot); + const pagesPath = new URL('./pages/', astroConfig.astroRoot); + const url = filepath.pathname.replace(pagesPath.pathname, '/').replace(/(index)?\.(astro|md)$/, ''); // build page in parallel with gathering runtimes await Promise.all([ - runtime.load(pagePath).then((result) => { - if (result.statusCode === 200) { - buildState[relPath] = { srcPath, contents: result.contents, contentType: 'text/html', encoding: 'utf8' }; - } + runtime.load(url).then((result) => { + if (result.statusCode !== 200) throw new Error((result as any).error); + const outFile = path.posix.join(url, '/index.html'); + buildState[outFile] = { + srcPath: filepath, + contents: result.contents, + contentType: 'text/html', + encoding: 'utf8', + }; }), gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }), ]); diff --git a/packages/astro/src/build/rss.ts b/packages/astro/src/build/rss.ts index e7a12da55..c755102db 100644 --- a/packages/astro/src/build/rss.ts +++ b/packages/astro/src/build/rss.ts @@ -3,17 +3,25 @@ import parser from 'fast-xml-parser'; import { canonicalURL } from './util.js'; /** Validates createCollection.rss */ -export function validateRSS(rss: CollectionRSS, filename: string): void { - if (!rss.title) throw new Error(`[${filename}] rss.title required`); - if (!rss.description) throw new Error(`[${filename}] rss.description required`); - if (typeof rss.item !== 'function') throw new Error(`[${filename}] rss.item() function required`); +export function validateRSS(rss: CollectionRSS, srcFile: string): void { + if (!rss.title) throw new Error(`[${srcFile}] rss.title required`); + if (!rss.description) throw new Error(`[${srcFile}] rss.description required`); + if (typeof rss.item !== 'function') throw new Error(`[${srcFile}] rss.item() function required`); +} + +type RSSInput = { data: T[]; site: string } & CollectionRSS; +interface RSSOptions { + srcFile: string; + feedURL: string; } /** Generate RSS 2.0 feed */ -export function generateRSS(input: { data: T[]; site: string } & CollectionRSS, filename: string): string { - let xml = `(input: RSSInput, options: RSSOptions): string { + const { srcFile, feedURL } = options; - validateRSS(input as any, filename); + validateRSS(input as any, srcFile); + + let xml = `(input: { data: T[]; site: string } & CollectionRS // title, description, customData xml += `<![CDATA[${input.title}]]>`; xml += ``; - xml += `${canonicalURL('/feed/' + filename + '.xml', input.site).href}`; + xml += `${canonicalURL(feedURL, input.site).href}`; if (typeof input.customData === 'string') xml += input.customData; // items - if (!Array.isArray(input.data) || !input.data.length) throw new Error(`[${filename}] data() returned no items. Can’t generate RSS feed.`); + if (!Array.isArray(input.data) || !input.data.length) throw new Error(`[${srcFile}] data() returned no items. Can’t generate RSS feed.`); for (const item of input.data) { xml += ``; const result = input.item(item); // validate - if (typeof result !== 'object') throw new Error(`[${filename}] rss.item() expected to return an object, returned ${typeof result}.`); - if (!result.title) throw new Error(`[${filename}] rss.item() returned object but required "title" is missing.`); - if (!result.link) throw new Error(`[${filename}] rss.item() returned object but required "link" is missing.`); + if (typeof result !== 'object') throw new Error(`[${srcFile}] rss.item() expected to return an object, returned ${typeof result}.`); + if (!result.title) throw new Error(`[${srcFile}] rss.item() returned object but required "title" is missing.`); + if (!result.link) throw new Error(`[${srcFile}] rss.item() returned object but required "link" is missing.`); xml += `<![CDATA[${result.title}]]>`; xml += `${canonicalURL(result.link, input.site).href}`; if (result.description) xml += ``; diff --git a/packages/astro/src/build/util.ts b/packages/astro/src/build/util.ts index 6b8834f43..902860bd8 100644 --- a/packages/astro/src/build/util.ts +++ b/packages/astro/src/build/util.ts @@ -2,13 +2,14 @@ import type { AstroConfig } from '../@types/astro'; import { performance } from 'perf_hooks'; import path from 'path'; -import { fileURLToPath, URL } from 'url'; +import { URL } from 'url'; /** Normalize URL to its canonical form */ export function canonicalURL(url: string, base?: string): URL { let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections) if (!path.extname(pathname)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if there’s no extension + pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() won’t) return new URL(pathname, base); } @@ -20,12 +21,14 @@ export function sortSet(set: Set): Set { /** Resolve final output URL */ export function getDistPath(specifier: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL }): string { if (specifier[0] === '/') return specifier; // assume absolute URLs are correct + const pagesDir = path.join(astroConfig.astroRoot.pathname, 'pages'); - const fileLoc = path.posix.join(path.posix.dirname(fileURLToPath(srcPath)), specifier); - const projectLoc = path.posix.relative(fileURLToPath(astroConfig.astroRoot), fileLoc); - const pagesDir = fileURLToPath(new URL('/pages', astroConfig.astroRoot)); + const fileLoc = path.posix.join(path.posix.dirname(srcPath.pathname), specifier); + const projectLoc = path.posix.relative(astroConfig.astroRoot.pathname, fileLoc); + + const isPage = fileLoc.includes(pagesDir); // if this lives above src/pages, return that URL - if (fileLoc.includes(pagesDir)) { + if (isPage) { const [, publicURL] = projectLoc.split(pagesDir); return publicURL || '/index.html'; // if this is missing, this is the root } diff --git a/packages/astro/src/compiler/codegen/content.ts b/packages/astro/src/compiler/codegen/content.ts index 1b630c912..fd7ac174a 100644 --- a/packages/astro/src/compiler/codegen/content.ts +++ b/packages/astro/src/compiler/codegen/content.ts @@ -1,16 +1,13 @@ import path from 'path'; -import { fdir, PathsOutput } from 'fdir'; -import { fileURLToPath } from 'url'; +import glob from 'tiny-glob/sync.js'; import slash from 'slash'; /** * Handling for import.meta.glob and import.meta.globEager */ - interface GlobOptions { namespace: string; filename: string; - projectRoot: URL; } interface GlobResult { @@ -20,36 +17,15 @@ interface GlobResult { code: string; } -const crawler = new fdir(); - /** General glob handling */ function globSearch(spec: string, { filename }: { filename: string }): string[] { try { - // Note: fdir’s glob requires you to do some work finding the closest non-glob folder. - // For example, this fails: .glob("./post/*.md").crawl("/…/src/pages") ❌ - // …but this doesn’t: .glob("*.md").crawl("/…/src/pages/post") ✅ - let globDir = ''; - let glob = spec; - for (const part of spec.split('/')) { - if (!part.includes('*')) { - // iterate through spec until first '*' is reached - globDir = path.posix.join(globDir, part); // this must be POSIX-style - glob = glob.replace(`${part}/`, ''); // move parent dirs off spec, and onto globDir - } else { - // at first '*', exit - break; - } - } - - const cwd = path.join(path.dirname(filename), globDir.replace(/\//g, path.sep)); // this must match OS (could be '/' or '\') - let found = crawler.glob(glob).crawlWithOptions(cwd, { includeBasePath: true }).sync() as PathsOutput; + const cwd = path.dirname(filename); + let found = glob(spec, { cwd, filesOnly: true }); if (!found.length) { throw new Error(`No files matched "${spec}" from ${filename}`); } - return found.map((importPath) => { - if (importPath.startsWith('http') || importPath.startsWith('.')) return importPath; - return './' + path.posix.join(globDir, path.posix.relative(slash(cwd), importPath)); - }); + return found.map((f) => slash(f[0] === '.' ? f : `./${f}`)); } catch (err) { throw new Error(`No files matched "${spec}" from ${filename}`); } diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index 6c7e8df07..2da1ceec5 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -1,13 +1,11 @@ -import type { CompileOptions } from '../../@types/compiler'; -import type { AstroConfig, ValidExtensionPlugins } from '../../@types/astro'; import type { Ast, Script, Style, TemplateNode } from 'astro-parser'; -import type { TransformResult } from '../../@types/astro'; +import type { CompileOptions } from '../../@types/compiler'; +import type { AstroConfig, TransformResult, ValidExtensionPlugins } from '../../@types/astro'; import 'source-map-support/register.js'; import eslexer from 'es-module-lexer'; import esbuild from 'esbuild'; import path from 'path'; -import { fileURLToPath } from 'url'; import { walk } from 'estree-walker'; import _babelGenerator from '@babel/generator'; import babelParser from '@babel/parser'; @@ -20,9 +18,9 @@ import { isFetchContent } from './utils.js'; import { yellow } from 'kleur/colors'; const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default; -const babelGenerator: typeof _babelGenerator = - // @ts-ignore - _babelGenerator.default; + +// @ts-ignore +const babelGenerator: typeof _babelGenerator = _babelGenerator.default; const { transformSync } = esbuild; interface Attribute { @@ -453,7 +451,6 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp // handle createCollection, if any if (createCollection) { - // TODO: improve this? while transforming in-place isn’t great, this happens at most once per-route const ast = babelParser.parse(createCollection, { sourceType: 'module', }); @@ -484,7 +481,7 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp const spec = (init as any).arguments[0].value; if (typeof spec !== 'string') break; - const globResult = fetchContent(spec, { namespace, filename: state.filename, projectRoot: compileOptions.astroConfig.projectRoot }); + const globResult = fetchContent(spec, { namespace, filename: state.filename }); let imports = ''; for (const importStatement of globResult.imports) { @@ -503,7 +500,7 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp // Astro.fetchContent() for (const [namespace, { spec }] of contentImports.entries()) { - const globResult = fetchContent(spec, { namespace, filename: state.filename, projectRoot: compileOptions.astroConfig.projectRoot }); + const globResult = fetchContent(spec, { namespace, filename: state.filename }); for (const importStatement of globResult.imports) { state.importExportStatements.add(importStatement); } @@ -605,7 +602,7 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`; } catch (err) { // handle errors in scope with filename - const rel = filename.replace(fileURLToPath(astroConfig.projectRoot), ''); + const rel = filename.replace(astroConfig.projectRoot.pathname, ''); // TODO: return actual codeframe here error(compileOptions.logging, rel, err.toString()); } diff --git a/packages/astro/src/config.ts b/packages/astro/src/config.ts index 682d39ac9..53ba69397 100644 --- a/packages/astro/src/config.ts +++ b/packages/astro/src/config.ts @@ -1,7 +1,7 @@ import type { AstroConfig } from './@types/astro'; import 'source-map-support/register.js'; -import { join as pathJoin, resolve as pathResolve } from 'path'; +import path from 'path'; import { existsSync } from 'fs'; /** Type util */ @@ -73,17 +73,13 @@ function normalizeConfig(userConfig: any, root: string): AstroConfig { /** Attempt to load an `astro.config.mjs` file */ export async function loadConfig(rawRoot: string | undefined, configFileName = 'astro.config.mjs'): Promise { - if (typeof rawRoot === 'undefined') { - rawRoot = process.cwd(); - } - - const root = pathResolve(rawRoot); - const astroConfigPath = pathJoin(root, configFileName); + const root = rawRoot ? path.resolve(rawRoot) : process.cwd(); + const astroConfigPath = new URL(`./${configFileName}`, `file://${root}/`); // load let config: any; if (existsSync(astroConfigPath)) { - config = configDefaults((await import(astroConfigPath)).default); + config = configDefaults((await import(astroConfigPath.href)).default); } else { config = configDefaults(); } diff --git a/packages/astro/src/search.ts b/packages/astro/src/search.ts index 96338dd0c..20f600d31 100644 --- a/packages/astro/src/search.ts +++ b/packages/astro/src/search.ts @@ -2,7 +2,7 @@ import 'source-map-support/register.js'; import { existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { fdir, PathsOutput } from 'fdir'; +import glob from 'tiny-glob/sync.js'; interface PageLocation { fileURL: URL; @@ -108,14 +108,9 @@ export function searchForPage(url: URL, astroRoot: URL): SearchResult { }; } -const crawler = new fdir(); - /** load a collection route */ function loadCollection(url: string, astroRoot: URL): { currentPage?: number; location: PageLocation } | undefined { - const pages = (crawler - .glob('**/*') - .crawl(path.join(fileURLToPath(astroRoot), 'pages')) - .sync() as PathsOutput).filter((filepath) => filepath.startsWith('$') || filepath.includes('/$')); + const pages = glob('**/$*.astro', { cwd: path.join(fileURLToPath(astroRoot), 'pages'), filesOnly: true }); for (const pageURL of pages) { const reqURL = new RegExp('^/' + pageURL.replace(/\$([^/]+)\.astro/, '$1') + '/?(.*)'); const match = url.match(reqURL); diff --git a/packages/astro/test/astro-collection.test.js b/packages/astro/test/astro-collection.test.js index e424d3871..2af4c1853 100644 --- a/packages/astro/test/astro-collection.test.js +++ b/packages/astro/test/astro-collection.test.js @@ -7,8 +7,8 @@ const Collections = suite('Collections'); setup(Collections, './fixtures/astro-collection'); -Collections('generates list & sorts successfully', async ({ runtime }) => { - const result = await runtime.load('/posts'); +Collections('shallow selector (*.md)', async ({ runtime }) => { + const result = await runtime.load('/shallow'); if (result.error) throw new Error(result.error); const $ = doc(result.contents); const urls = [ @@ -16,11 +16,24 @@ Collections('generates list & sorts successfully', async ({ runtime }) => { return $(this).attr('href'); }), ]; - assert.equal(urls, ['/post/nested/a', '/post/three', '/post/two']); + // assert they loaded in newest -> oldest order (not alphabetical) + assert.equal(urls, ['/post/three', '/post/two', '/post/one']); +}); + +Collections('deep selector (**/*.md)', async ({ runtime }) => { + const result = await runtime.load('/nested'); + if (result.error) throw new Error(result.error); + const $ = doc(result.contents); + const urls = [ + ...$('#posts a').map(function () { + return $(this).attr('href'); + }), + ]; + assert.equal(urls, ['/post/nested/a', '/post/three', '/post/two', '/post/one']); }); Collections('generates pagination successfully', async ({ runtime }) => { - const result = await runtime.load('/posts'); + const result = await runtime.load('/paginated'); if (result.error) throw new Error(result.error); const $ = doc(result.contents); const prev = $('#prev-page'); diff --git a/packages/astro/test/astro-markdown.test.js b/packages/astro/test/astro-markdown.test.js index 8c467bf9d..97be990d8 100644 --- a/packages/astro/test/astro-markdown.test.js +++ b/packages/astro/test/astro-markdown.test.js @@ -1,47 +1,14 @@ -import { existsSync, promises as fsPromises } from 'fs'; -import { join } from 'path'; -import { fileURLToPath } from 'url'; import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import { createRuntime } from '#astro/runtime'; -import { build } from '#astro/build'; -import { loadConfig } from '#astro/config'; import { doc } from './test-utils.js'; - -const { rmdir, readFile } = fsPromises; +import { setup, setupBuild } from './helpers.js'; const Markdown = suite('Astro Markdown'); -let runtime, setupError, fixturePath, astroConfig; +setup(Markdown, './fixtures/astro-markdown'); +setupBuild(Markdown, './fixtures/astro-markdown'); -Markdown.before(async () => { - fixturePath = fileURLToPath(new URL('./fixtures/astro-markdown', import.meta.url)); - - astroConfig = await loadConfig(fixturePath); - - const logging = { - level: 'error', - dest: process.stderr, - }; - - try { - runtime = await createRuntime(astroConfig, { logging }); - } catch (err) { - console.error(err); - setupError = err; - } -}); - -Markdown.after(async () => { - (await runtime) && runtime.shutdown(); - rmdir(join(fixturePath, 'dist'), { recursive: true }); -}); - -Markdown('No errors creating a runtime', () => { - assert.equal(setupError, undefined); -}); - -Markdown('Can load markdown pages with hmx', async () => { +Markdown('Can load markdown pages with hmx', async ({ runtime }) => { const result = await runtime.load('/post'); if (result.error) throw new Error(result.error); @@ -50,7 +17,7 @@ Markdown('Can load markdown pages with hmx', async () => { assert.ok($('#test').length, 'There is a div added via a component from markdown'); }); -Markdown('Can load more complex jsxy stuff', async () => { +Markdown('Can load more complex jsxy stuff', async ({ runtime }) => { const result = await runtime.load('/complex'); if (result.error) throw new Error(result.error); @@ -59,13 +26,14 @@ Markdown('Can load more complex jsxy stuff', async () => { assert.equal($el.text(), 'Hello world'); }); -Markdown('Bundles client-side JS for prod', async () => { - await build(astroConfig); - - const complexHtml = await readFile(join(fixturePath, './dist/complex/index.html'), 'utf-8'); +Markdown('Bundles client-side JS for prod', async (context) => { + await context.build(); + const complexHtml = await context.readFile('/complex/index.html'); assert.match(complexHtml, `import("/_astro/components/Counter.js"`); - assert.ok(existsSync(join(fixturePath, `./dist/_astro/components/Counter.js`)), 'Counter.jsx is bundled for prod'); + + const counterJs = await context.readFile('/_astro/components/Counter.js'); + assert.ok(counterJs, 'Counter.jsx is bundled for prod'); }); Markdown.run(); diff --git a/packages/astro/test/astro-rss.test.js b/packages/astro/test/astro-rss.test.js index 055150362..18dcb667b 100644 --- a/packages/astro/test/astro-rss.test.js +++ b/packages/astro/test/astro-rss.test.js @@ -6,9 +6,7 @@ const RSS = suite('RSS Generation'); setupBuild(RSS, './fixtures/astro-rss'); -const snapshot = - `<![CDATA[MF Doomcast]]>https://mysite.dev/feed/episodes.xmlen-usMF Doom<![CDATA[Rap Snitch Knishes (feat. Mr. Fantastik)]]>https://mysite.dev/episode/rap-snitch-knishes/Tue, 16 Nov 2004 00:00:00 GMTmusic172true<![CDATA[Fazers]]>https://mysite.dev/episode/fazers/Thu, 03 Jul 2003 00:00:00 GMTmusic197true<![CDATA[Rhymes Like Dimes (feat. Cucumber Slice)]]>https://mysite.dev/episode/rhymes-like-dimes/Tue, 19 Oct 1999 00:00:00 GMTmusic259true'; +const snapshot = `<![CDATA[MF Doomcast]]>https://mysite.dev/feed/episodes.xmlen-usMF Doom<![CDATA[Rap Snitch Knishes (feat. Mr. Fantastik)]]>https://mysite.dev/episode/rap-snitch-knishes/Tue, 16 Nov 2004 00:00:00 GMTmusic172true<![CDATA[Fazers]]>https://mysite.dev/episode/fazers/Thu, 03 Jul 2003 00:00:00 GMTmusic197true<![CDATA[Rhymes Like Dimes (feat. Cucumber Slice)]]>https://mysite.dev/episode/rhymes-like-dimes/Tue, 19 Oct 1999 00:00:00 GMTmusic259true`; RSS('Generates RSS correctly', async (context) => { await context.build(); diff --git a/packages/astro/test/config-port.test.js b/packages/astro/test/config-port.test.js index 000c5575b..b8e2d3ee9 100644 --- a/packages/astro/test/config-port.test.js +++ b/packages/astro/test/config-port.test.js @@ -1,3 +1,4 @@ +import { fileURLToPath } from 'url'; import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import { runDevServer } from './helpers.js'; @@ -6,8 +7,9 @@ import { loadConfig } from '#astro/config'; const ConfigPort = suite('Config path'); const root = new URL('./fixtures/config-port/', import.meta.url); + ConfigPort('can be specified in the astro config', async (context) => { - const astroConfig = await loadConfig(root.pathname); + const astroConfig = await loadConfig(fileURLToPath(root)); assert.equal(astroConfig.devOptions.port, 3001); }); diff --git a/packages/astro/test/fixtures/astro-collection/src/pages/$nested.astro b/packages/astro/test/fixtures/astro-collection/src/pages/$nested.astro new file mode 100644 index 000000000..9e87d3588 --- /dev/null +++ b/packages/astro/test/fixtures/astro-collection/src/pages/$nested.astro @@ -0,0 +1,27 @@ +--- +export let collection: any; + +export async function createCollection() { + return { + async data() { + let data = Astro.fetchContent('./post/**/*.md'); + data.sort((a, b) => new Date(b.date) - new Date(a.date)); + return data; + } + }; +} +--- + +
+{collection.data.map((post) => ( + +))} +
+ + diff --git a/packages/astro/test/fixtures/astro-collection/src/pages/$paginated.astro b/packages/astro/test/fixtures/astro-collection/src/pages/$paginated.astro new file mode 100644 index 000000000..5e83aab05 --- /dev/null +++ b/packages/astro/test/fixtures/astro-collection/src/pages/$paginated.astro @@ -0,0 +1,28 @@ +--- +export let collection: any; + +export async function createCollection() { + return { + async data() { + let data = Astro.fetchContent('./post/**/*.md'); + data.sort((a, b) => new Date(b.date) - new Date(a.date)); + return data; + }, + pageSize: 1 + }; +} +--- + +
+{collection.data.map((post) => ( + +))} +
+ + diff --git a/packages/astro/test/fixtures/astro-collection/src/pages/$posts.astro b/packages/astro/test/fixtures/astro-collection/src/pages/$shallow.astro similarity index 97% rename from packages/astro/test/fixtures/astro-collection/src/pages/$posts.astro rename to packages/astro/test/fixtures/astro-collection/src/pages/$shallow.astro index ccd5d726d..01e30f398 100644 --- a/packages/astro/test/fixtures/astro-collection/src/pages/$posts.astro +++ b/packages/astro/test/fixtures/astro-collection/src/pages/$shallow.astro @@ -8,7 +8,7 @@ export async function createCollection() { data.sort((a, b) => new Date(b.date) - new Date(a.date)); return data; }, - pageSize: 3 + pageSize: 4 }; } --- diff --git a/tools/prettier-plugin-astro/test/astro-prettier.test.js b/tools/prettier-plugin-astro/test/astro-prettier.test.js index cf0f0c87c..fbc847228 100644 --- a/tools/prettier-plugin-astro/test/astro-prettier.test.js +++ b/tools/prettier-plugin-astro/test/astro-prettier.test.js @@ -5,7 +5,7 @@ import { promises as fs } from 'fs'; import { fileURLToPath } from 'url'; const Prettier = suite('Prettier formatting'); -const readFile = (path) => fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url))).then((res) => res.toString()); +const readFile = (path) => fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url))).then((res) => res.toString().replace(/\r\n/g, '\n')); /** * Utility to get `[src, out]` files diff --git a/yarn.lock b/yarn.lock index b08429b13..dee9e922d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4626,7 +4626,7 @@ fd-slicer@~1.1.0: fdir@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/fdir/-/fdir-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-5.0.0.tgz#a40b5d9adfb530daeca55558e8ad87ec14a44769" integrity sha512-cteqwWMA43lEmgwOg5HSdvhVFD39vHjQDhZkRMlKmeoNPtSSgUw1nUypydiY2upMdGiBFBZvNBDbnoBh0yCzaQ== figures@^3.0.0, figures@^3.2.0: @@ -9801,13 +9801,20 @@ safe-regex@^1.1.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@^1.3.0, sass@^1.32.8: +sass@^1.3.0: version "1.32.12" resolved "https://registry.npmjs.org/sass/-/sass-1.32.12.tgz" integrity sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA== dependencies: chokidar ">=3.0.0 <4.0.0" +sass@^1.32.13: + version "1.32.13" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.13.tgz#8d29c849e625a415bce71609c7cf95e15f74ed00" + integrity sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA== + dependencies: + chokidar ">=3.0.0 <4.0.0" + scheduler@^0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz" @@ -11050,7 +11057,7 @@ tiny-emitter@^2.0.0: tiny-glob@^0.2.8: version "0.2.8" - resolved "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.8.tgz" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.8.tgz#b2792c396cc62db891ffa161fe8b33e76123e531" integrity sha512-vkQP7qOslq63XRX9kMswlby99kyO5OvKptw7AMwBVMjXEI7Tb61eoI5DydyEMOseyGS5anDN1VPoVxEvH01q8w== dependencies: globalyzer "0.1.0"