Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rfc/issue 1167 content as data #1266

Merged
merged 36 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
df8fdff
initial implementation of content collections with rich frontmatter s…
thescientist13 Aug 10, 2024
8db9df5
add test cases for collections and prerendering
thescientist13 Aug 11, 2024
331bce1
rename interpolateFrontmatter to activeFrontmatter
thescientist13 Aug 24, 2024
6cf8dc6
refactor id and lable graph properties
thescientist13 Aug 24, 2024
c4a0d4a
refactor graph title behavior
thescientist13 Aug 25, 2024
bf0d8d1
full graph and graphql plugin refactoring
thescientist13 Sep 6, 2024
3679424
update website for new graph refactoring
thescientist13 Sep 6, 2024
988a53e
make sure active frontmatter is enabled for title substition
thescientist13 Sep 6, 2024
dd85d71
restore header nav ordering
thescientist13 Sep 6, 2024
139670f
comment cleanup
thescientist13 Sep 6, 2024
97e4a70
eslint ignore
thescientist13 Sep 6, 2024
e89b79f
support multiple import maps
thescientist13 Sep 13, 2024
ce9109f
add id to graph and refactor usage
thescientist13 Sep 15, 2024
d1e1ce7
refactoring pagePath to pageHref
thescientist13 Sep 15, 2024
a070bc7
update test cases
thescientist13 Sep 15, 2024
0afaf50
rename data/queries.js to client.js
thescientist13 Sep 21, 2024
acee4f9
handle default title from graph and provide default graph content as …
thescientist13 Sep 21, 2024
c47394b
handle default title from graph and provide default graph content as …
thescientist13 Sep 21, 2024
f0e937f
refactor content as data handling to its own plugin
thescientist13 Sep 21, 2024
3a2f05d
refactor for better windows interop
thescientist13 Sep 21, 2024
f1c31d2
misc refactoring for active frontmatter
thescientist13 Sep 21, 2024
f913350
refactor outputPath to outputHref
thescientist13 Sep 28, 2024
0b9ebb0
add labels for custom side nav output
thescientist13 Sep 28, 2024
7501959
refresh content as data and GraphQL docs
thescientist13 Sep 29, 2024
f533638
update for new docs redirects
thescientist13 Sep 29, 2024
2c5227e
filter hidden files and improve unsupported page format detection mes…
thescientist13 Oct 12, 2024
29d7406
opt-on content as data config and misc refactoring and TODOs
thescientist13 Oct 13, 2024
14eaf7f
rename test case
thescientist13 Oct 13, 2024
c9a994e
update docs for content as data config option and patterns
thescientist13 Oct 13, 2024
5c769dd
conslidate content as data into dev server
thescientist13 Oct 17, 2024
750c607
misc refactoring
thescientist13 Oct 17, 2024
08bbaa1
content as data import map test cases
thescientist13 Oct 17, 2024
7e3ea00
fix selectors in test cases
thescientist13 Oct 17, 2024
7c65b39
rename test cases
thescientist13 Oct 17, 2024
db5f4eb
consolidate configuration options and update docs
thescientist13 Oct 18, 2024
4069c33
rename test cases
thescientist13 Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion greenwood.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default {
workspace: new URL('./www/', import.meta.url),
optimization: 'inline',
staticRouter: true,
interpolateFrontmatter: true,
activeContent: true,
plugins: [
greenwoodPluginGraphQL(),
greenwoodPluginPolyfills({
Expand Down
17 changes: 16 additions & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,19 @@

[[redirects]]
from = "/docs/tech-stack/"
to = "/about/tech-stack/"
to = "/about/tech-stack/"

[[redirects]]
from = "/docs/menus/:splat"
to = "/docs/data/"
status = 200

[[redirects]]
from = "/docs/data/#external-sources"
to = "/docs/data/#pages-data"
status = 200

[[redirects]]
from = "/docs/data/#internal-sources"
to = "/docs/data/#pages-data"
status = 200
20 changes: 16 additions & 4 deletions packages/cli/src/commands/build.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { bundleCompilation } from '../lifecycles/bundle.js';
import { checkResourceExists } from '../lib/resource-utils.js';
import { copyAssets } from '../lifecycles/copy.js';
import { getDevServer } from '../lifecycles/serve.js';
import fs from 'fs/promises';
import { preRenderCompilationWorker, preRenderCompilationCustom, staticRenderCompilation } from '../lifecycles/prerender.js';
import { ServerInterface } from '../lib/server-interface.js';
Expand All @@ -10,25 +11,26 @@ const runProductionBuild = async (compilation) => {
return new Promise(async (resolve, reject) => {

try {
const { prerender } = compilation.config;
const { prerender, activeContent, plugins } = compilation.config;
const outputDir = compilation.context.outputDir;
const prerenderPlugin = compilation.config.plugins.find(plugin => plugin.type === 'renderer')
? compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider(compilation)
: {};
const adapterPlugin = compilation.config.plugins.find(plugin => plugin.type === 'adapter')
? compilation.config.plugins.find(plugin => plugin.type === 'adapter').provider(compilation)
: null;
const shouldPrerender = prerender || prerenderPlugin.prerender;

if (!await checkResourceExists(outputDir)) {
await fs.mkdir(outputDir, {
recursive: true
});
}

if (prerender || prerenderPlugin.prerender) {
// start any servers if needed
if (shouldPrerender || (activeContent && shouldPrerender)) {
// start any of the user's server plugins if needed
const servers = [...compilation.config.plugins.filter((plugin) => {
return plugin.type === 'server';
return plugin.type === 'server' && !plugin.isGreenwoodDefaultPlugin;
}).map((plugin) => {
const provider = plugin.provider(compilation);

Expand All @@ -39,6 +41,16 @@ const runProductionBuild = async (compilation) => {
return provider;
})];

if (activeContent) {
(await getDevServer({
...compilation,
// prune for the content as data plugin and start the dev server with only that plugin enabled
plugins: [plugins.find(plugin => plugin.name === 'plugin-active-content')]
})).listen(compilation.config.devServer.port, () => {
console.info('Initializing active content...');
});
}

await Promise.all(servers.map(async (server) => {
await server.start();

Expand Down
43 changes: 20 additions & 23 deletions packages/cli/src/config/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
name: 'greenwood-sync-ssr-pages-entry-point-output-paths',
generateBundle(options, bundle) {
const { basePath } = compilation.config;
const { scratchDir } = compilation.context;
const { scratchDir, outputDir } = compilation.context;

// map rollup bundle names back to original SSR pages for syncing input <> output bundle names
Object.keys(bundle).forEach((key) => {
Expand All @@ -178,7 +178,7 @@

compilation.graph.forEach((page, idx) => {
if (page.route === route) {
compilation.graph[idx].outputPath = key;
compilation.graph[idx].outputHref = new URL(`./${key}`, outputDir).href;
}
});
}
Expand All @@ -192,7 +192,7 @@
name: 'greenwood-sync-api-routes-output-paths',
generateBundle(options, bundle) {
const { basePath } = compilation.config;
const { apisDir } = compilation.context;
const { apisDir, outputDir } = compilation.context;

// map rollup bundle names back to original SSR pages for syncing input <> output bundle names
Object.keys(bundle).forEach((key) => {
Expand All @@ -206,7 +206,7 @@

compilation.manifest.apis.set(route, {
...api,
outputPath: `/api/${key}`
outputHref: new URL(`./api/${key}`, outputDir).href
});
}
}
Expand Down Expand Up @@ -353,8 +353,9 @@
if (`${compilation.context.apisDir.pathname}${idAssetName}`.indexOf(normalizedId) >= 0) {
for (const entry of compilation.manifest.apis.keys()) {
const apiRoute = compilation.manifest.apis.get(entry);
const pagePath = apiRoute.pageHref.replace(`${compilation.context.pagesDir}api/`, '');

if (normalizedId.endsWith(apiRoute.path)) {
if (normalizedId.endsWith(pagePath)) {
const assets = apiRoute.assets || [];

assets.push(assetUrl.url.href);
Expand All @@ -366,7 +367,7 @@
}
}
} else {
// TODO figure out how to handle URL chunk from SSR pages

Check warning on line 370 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO figure out how to handle URL chunk...'

Check warning on line 370 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO figure out how to handle URL chunk...'

Check warning on line 370 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO figure out how to handle URL chunk...'

Check warning on line 370 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO figure out how to handle URL chunk...'
// https://github.com/ProjectEvergreen/greenwood/issues/1163
}

Expand Down Expand Up @@ -643,21 +644,21 @@
};

const getRollupConfigForApiRoutes = async (compilation) => {
const { outputDir, pagesDir, apisDir } = compilation.context;
const { outputDir } = compilation.context;

return [...compilation.manifest.apis.values()]
.map(api => normalizePathnameForWindows(new URL(`.${api.path}`, pagesDir)))
.map((filepath) => {
// account for windows pathname shenanigans by "casting" filepath to a URL first
const ext = filepath.split('.').pop();
const entryName = new URL(`file://${filepath}`).pathname.replace(apisDir.pathname, '').replace(/\//g, '-').replace(`.${ext}`, '');
.map((api) => {
const { id, pageHref } = api;

return { id, inputPath: normalizePathnameForWindows(new URL(pageHref)) };
})
.map(({ id, inputPath }) => {
return {
input: filepath,
input: inputPath,
output: {
dir: `${normalizePathnameForWindows(outputDir)}/api`,
entryFileNames: `${entryName}.js`,
chunkFileNames: `${entryName}.[hash].js`
entryFileNames: `${id}.js`,
chunkFileNames: `${id}.[hash].js`
},
plugins: [
greenwoodResourceLoader(compilation),
Expand Down Expand Up @@ -696,20 +697,16 @@
});
};

const getRollupConfigForSsrPages = async (compilation, input) => {
const getRollupConfigForSsrPages = async (compilation, inputs) => {
const { outputDir } = compilation.context;

return input.map((filepath) => {
const ext = filepath.split('.').pop();
// account for windows pathname shenanigans by "casting" filepath to a URL first
const entryName = new URL(`file://${filepath}`).pathname.replace(compilation.context.scratchDir.pathname, '').replace('/', '-').replace(`.${ext}`, '');

return inputs.map(({ id, inputPath }) => {
return {
input: filepath,
input: inputPath,
output: {
dir: normalizePathnameForWindows(outputDir),
entryFileNames: `${entryName}.route.js`,
chunkFileNames: `${entryName}.route.chunk.[hash].js`
entryFileNames: `${id}.route.js`,
chunkFileNames: `${id}.route.chunk.[hash].js`
},
plugins: [
greenwoodResourceLoader(compilation),
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/data/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const CONTENT_STATE = globalThis.__CONTENT_AS_DATA_STATE__ ?? false; // eslint-disable-line no-underscore-dangle
const PORT = globalThis?.__CONTENT_SERVER__?.PORT ?? 1985; // eslint-disable-line no-underscore-dangle
const BASE_PATH = globalThis?.__GWD_BASE_PATH__ ?? ''; // eslint-disable-line no-underscore-dangle

async function getContentAsData(key = '') {
return CONTENT_STATE
? await fetch(`${window.location.origin}${BASE_PATH}/data-${key.replace(/\//g, '_')}.json`)
.then(resp => resp.json())
: await fetch(`http://localhost:${PORT}${BASE_PATH}/___graph.json`, { headers: { 'X-CONTENT-KEY': key } })
.then(resp => resp.json());
}

async function getContent() {
return await getContentAsData('graph');
}

async function getContentByCollection(collection = '') {
return await getContentAsData(`collection-${collection}`);
}

async function getContentByRoute(route = '') {
return await getContentAsData(`route-${route}`);
}

export { getContent, getContentByCollection, getContentByRoute };
23 changes: 23 additions & 0 deletions packages/cli/src/lib/content-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const activeFrontmatterKeys = ['route', 'label', 'title', 'id'];

function cleanContentCollection(collection = []) {
return collection.map((page) => {
let prunedPage = {};

Object.keys(page).forEach((key) => {
if ([...activeFrontmatterKeys, 'data'].includes(key)) {
prunedPage[key] = page[key];
}
});

return {
...prunedPage,
title: prunedPage.title || prunedPage.label
};
});
}

export {
activeFrontmatterKeys,
cleanContentCollection
};
40 changes: 22 additions & 18 deletions packages/cli/src/lib/layout-utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* eslint-disable complexity */
import fs from 'fs/promises';
import htmlparser from 'node-html-parser';
import { checkResourceExists } from './resource-utils.js';
import { Worker } from 'worker_threads';

async function getCustomPageLayoutsFromPlugins(compilation, layoutName) {
// TODO confirm context plugins work for SSR
// TODO support context plugins for more than just HTML files
const contextPlugins = compilation.config.plugins.filter((plugin) => {
return plugin.type === 'context';
}).map((plugin) => {
Expand All @@ -30,10 +29,10 @@ async function getCustomPageLayoutsFromPlugins(compilation, layoutName) {
return customLayoutLocations;
}

async function getPageLayout(filePath, compilation, layout) {
async function getPageLayout(pageHref = '', compilation, layout) {
const { config, context } = compilation;
const { layoutsDir, userLayoutsDir, pagesDir, projectDirectory } = context;
const filePathUrl = new URL(`${filePath}`, projectDirectory);
const { layoutsDir, userLayoutsDir, pagesDir } = context;
const filePathUrl = pageHref && pageHref !== '' ? new URL(pageHref) : pageHref;
const customPageFormatPlugins = config.plugins
.filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin)
.map(plugin => plugin.provider(compilation));
Expand All @@ -43,13 +42,13 @@ async function getPageLayout(filePath, compilation, layout) {
&& await customPageFormatPlugins[0].shouldServe(filePathUrl);
const customPluginDefaultPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, 'page');
const customPluginPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, layout);
const extension = filePath.split('.').pop();
const is404Page = filePath.startsWith('404') && extension === 'html';
const extension = pageHref?.split('.')?.pop();
const is404Page = pageHref?.endsWith('404.html') && extension === 'html';
const hasCustomStaticLayout = await checkResourceExists(new URL(`./${layout}.html`, userLayoutsDir));
const hasCustomDynamicLayout = await checkResourceExists(new URL(`./${layout}.js`, userLayoutsDir));
const hasPageLayout = await checkResourceExists(new URL('./page.html', userLayoutsDir));
const hasCustom404Page = await checkResourceExists(new URL('./404.html', pagesDir));
const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(`./${filePath}`, projectDirectory));
const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(pageHref));
let contents;

if (layout && (customPluginPageLayouts.length > 0 || hasCustomStaticLayout)) {
Expand Down Expand Up @@ -108,11 +107,11 @@ async function getPageLayout(filePath, compilation, layout) {
}

/* eslint-disable-next-line complexity */
async function getAppLayout(pageLayoutContents, compilation, customImports = [], frontmatterTitle) {
async function getAppLayout(pageLayoutContents, compilation, customImports = [], matchingRoute) {
const activeFrontmatterTitleKey = '${globalThis.page.title}';
const enableHud = compilation.config.devServer.hud;
const { layoutsDir, userLayoutsDir } = compilation.context;
const userStaticAppLayoutUrl = new URL('./app.html', userLayoutsDir);
// TODO support more than just .js files
const userDynamicAppLayoutUrl = new URL('./app.js', userLayoutsDir);
const userHasStaticAppLayout = await checkResourceExists(userStaticAppLayoutUrl);
const userHasDynamicAppLayout = await checkResourceExists(userDynamicAppLayoutUrl);
Expand Down Expand Up @@ -193,20 +192,25 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [],
const appBody = appRoot.querySelector('body') ? appRoot.querySelector('body').innerHTML : '';
const pageBody = pageRoot && pageRoot.querySelector('body') ? pageRoot.querySelector('body').innerHTML : '';
const pageTitle = pageRoot && pageRoot.querySelector('head title');
const hasInterpolatedFrontmatter = pageTitle && pageTitle.rawText.indexOf('${globalThis.page.title}') >= 0
|| appTitle && appTitle.rawText.indexOf('${globalThis.page.title}') >= 0;
const hasActiveFrontmatterTitle = compilation.config.activeContent && (pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0
|| appTitle && appTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0);
let title;

const title = hasInterpolatedFrontmatter // favor frontmatter interpolation first
? pageTitle && pageTitle.rawText
if (hasActiveFrontmatterTitle) {
const text = pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0
? pageTitle.rawText
: appTitle.rawText
: frontmatterTitle // otherwise, work in order of specificity from page -> page layout -> app layout
? frontmatterTitle
: appTitle.rawText;

title = text.replace(activeFrontmatterTitleKey, matchingRoute.title || matchingRoute.label);
} else {
title = matchingRoute.title
? matchingRoute.title
: pageTitle && pageTitle.rawText
? pageTitle.rawText
: appTitle && appTitle.rawText
? appTitle.rawText
: 'My App';
: matchingRoute.label;
}

const mergedHtml = pageRoot && pageRoot.querySelector('html').rawAttrs !== ''
? `<html ${pageRoot.querySelector('html').rawAttrs}>`
Expand Down
36 changes: 26 additions & 10 deletions packages/cli/src/lib/walker-package-ranger.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,33 @@ async function walkPackageJson(packageJson = {}) {
return importMap;
}

function mergeImportMap(html = '', map = {}) {
// es-modules-shims breaks on dangling commas in an importMap :/
const danglingComma = html.indexOf('"imports": {}') > 0 ? '' : ',';
const importMap = JSON.stringify(map).replace('}', '').replace('{', '');

const merged = html.replace('"imports": {', `
"imports": {
${importMap}${danglingComma}
`);
function mergeImportMap(html = '', map = {}, shouldShim = false) {
const importMapType = shouldShim ? 'importmap-shim' : 'importmap';
const hasImportMap = html.indexOf(`script type="${importMapType}"`) > 0;
const danglingComma = hasImportMap ? ',' : '';
const importMap = JSON.stringify(map, null, 2).replace('}', '').replace('{', '');

if (Object.entries(map).length === 0) {
return html;
}

return merged;
if (hasImportMap) {
return html.replace('"imports": {', `
"imports": {
${importMap}${danglingComma}
`);
} else {
return html.replace('<head>', `
<head>
<script type="${importMapType}">
{
"imports": {
${importMap}
}
}
</script>
`);
}
}

export {
Expand Down
Loading
Loading