Skip to content

Commit

Permalink
Feature/issue 1116 base path configuration (#1135)
Browse files Browse the repository at this point in the history
* base path configuration support WIP

* base path examples

* refactor base configuraton within the graph

* integrate base path into script, style and link tags

* inegrate base path into graphql plugin

* integrate base path into static router

* all test cases passing

* display base path in server URL terminal output

* add base path documentation

* handle route depths and base path in graphql resolvers

* refactor graphql route depths and base path handling

* add basic base path test cases for develop and serve commands

* fix linting

* remove describe.only

* upgrade github pages docs with base path callout and configuration

* add image tag example to manual base path prefixing in docs

* add base path config serve based test cases

* refactor test cases to use fetch

* flesh out base path test cases for proxies, API routes, and SSR pages

* refactor specs to use fetch

* add base path support to adapter plugins

* restore post rebase misses

* have CLI serialize base path into all pages

* document base path persistance

* remove demo code
  • Loading branch information
thescientist13 authored Nov 3, 2023
1 parent 34156fc commit d2a6103
Show file tree
Hide file tree
Showing 79 changed files with 1,709 additions and 153 deletions.
6 changes: 4 additions & 2 deletions packages/cli/src/commands/develop.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ const runDevServer = async (compilation) => {
return new Promise(async (resolve, reject) => {

try {
const { port } = compilation.config.devServer;
const { basePath, devServer } = compilation.config;
const { port } = devServer;
const postfixSlash = basePath === '' ? '' : '/';

(await getDevServer(compilation)).listen(port, () => {

console.info(`Started local development server at http://localhost:${port}`);
console.info(`Started local development server at http://localhost:${port}${basePath}${postfixSlash}`);

const servers = [...compilation.config.plugins.filter((plugin) => {
return plugin.type === 'server';
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ const runProdServer = async (compilation) => {
return new Promise(async (resolve, reject) => {

try {
const port = compilation.config.port;
const { basePath, port } = compilation.config;
const postfixSlash = basePath === '' ? '' : '/';
const hasApisDir = await checkResourceExists(compilation.context.apisDir);
const hasDynamicRoutes = compilation.graph.find(page => page.isSSR && !page.prerender);
const server = (hasDynamicRoutes && !compilation.config.prerender) || hasApisDir ? getHybridServer : getStaticServer;

(await server(compilation)).listen(port, () => {
console.info(`Started server at http://localhost:${port}`);
console.info(`Started server at http://localhost:${port}${basePath}${postfixSlash}`);
});
} catch (err) {
reject(err);
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/lib/templating-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,16 @@ async function getAppTemplate(pageTemplateContents, context, customImports = [],
return mergedTemplateContents;
}

async function getUserScripts (contents, context) {
async function getUserScripts (contents, compilation) {
const { context, config } = compilation;

contents = contents.replace('<head>', `
<head>
<script data-gwd="base-path">
globalThis.__GWD_BASE_PATH__ = '${config.basePath}';
</script>
`);

// TODO get rid of lit polyfills in core
// https://github.com/ProjectEvergreen/greenwood/issues/728
// https://lit.dev/docs/tools/requirements/#polyfills
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async function optimizeStaticPages(compilation, plugins) {
.filter(page => !page.isSSR || (page.isSSR && page.prerender) || (page.isSSR && compilation.config.prerender))
.map(async (page) => {
const { route, outputPath } = page;
const outputDirUrl = new URL(`.${route}`, outputDir);
const outputDirUrl = new URL(`.${outputPath.replace('index.html', '').replace('404.html', '')}`, outputDir);
const url = new URL(`http://localhost:${compilation.config.port}${route}`);
const contents = await fs.readFile(new URL(`./${outputPath}`, scratchDir), 'utf-8');
const headers = new Headers({ 'Content-Type': 'text/html' });
Expand All @@ -70,7 +70,7 @@ async function optimizeStaticPages(compilation, plugins) {
// clean up optimization markers
const body = (await response.text()).replace(/data-gwd-opt=".*[a-z]"/g, '');

await fs.writeFile(new URL(`./${outputPath}`, outputDir), body);
await fs.writeFile(new URL(`.${outputPath}`, outputDir), body);
})
);
}
Expand Down Expand Up @@ -201,7 +201,7 @@ async function bundleSsrPages(compilation) {

staticHtml = data.template ? data.template : await getPageTemplate(staticHtml, compilation.context, template, []);
staticHtml = await getAppTemplate(staticHtml, compilation.context, imports, [], false, title);
staticHtml = await getUserScripts(staticHtml, compilation.context);
staticHtml = await getUserScripts(staticHtml, compilation);
staticHtml = await (await htmlOptimizer.optimize(new URL(`http://localhost:8080${route}`), new Response(staticHtml))).text();
staticHtml = staticHtml.replace(/[`\\$]/g, '\\$&'); // https://stackoverflow.com/a/75688937/417806

Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/lifecycles/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const defaultConfig = {
extensions: []
},
port: 8080,
basePath: '',
optimization: optimizations[0],
interpolateFrontmatter: false,
plugins: greenwoodPlugins,
Expand Down Expand Up @@ -75,7 +76,7 @@ const readAndMergeConfig = async() => {

if (hasConfigFile) {
const userCfgFile = (await import(configUrl)).default;
const { workspace, devServer, markdown, optimization, plugins, port, prerender, staticRouter, pagesDirectory, templatesDirectory, interpolateFrontmatter } = userCfgFile;
const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, templatesDirectory, interpolateFrontmatter } = userCfgFile;

// workspace validation
if (workspace) {
Expand Down Expand Up @@ -188,6 +189,15 @@ const readAndMergeConfig = async() => {
}
}

if (basePath) {
// eslint-disable-next-line max-depth
if (typeof basePath !== 'string') {
reject(`Error: greenwood.config.js basePath must be a string. Passed value was: ${basePath}`);
} else {
customConfig.basePath = basePath;
}
}

if (pagesDirectory && typeof pagesDirectory === 'string') {
customConfig.pagesDirectory = pagesDirectory;
} else if (pagesDirectory) {
Expand Down
19 changes: 10 additions & 9 deletions packages/cli/src/lifecycles/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ const generateGraph = async (compilation) => {

return new Promise(async (resolve, reject) => {
try {
const { context } = compilation;
const { context, config } = compilation;
const { basePath } = config;
const { apisDir, pagesDir, projectDirectory, userWorkspace } = context;
let graph = [{
outputPath: 'index.html',
outputPath: '/index.html',
filename: 'index.html',
path: '/',
route: '/',
route: `${basePath}/`,
id: 'index',
label: 'Index',
data: {},
Expand Down Expand Up @@ -212,10 +213,10 @@ const generateGraph = async (compilation) => {
imports,
resources: [],
outputPath: route === '/404/'
? '404.html'
? '/404.html'
: `${route}index.html`,
path: filePath,
route,
route: `${basePath}${route}`,
template,
title,
isSSR: !isStatic,
Expand All @@ -240,7 +241,7 @@ const generateGraph = async (compilation) => {
} else {
const extension = filenameUrl.pathname.split('.').pop();
const relativeApiPath = filenameUrl.pathname.replace(userWorkspace.pathname, '/');
const route = relativeApiPath.replace(`.${extension}`, '');
const route = `${basePath}${relativeApiPath.replace(`.${extension}`, '')}`;

if (extension !== 'js') {
console.warn(`${filenameUrl} is not a JavaScript file, skipping...`);
Expand Down Expand Up @@ -280,7 +281,7 @@ const generateGraph = async (compilation) => {

graph = await checkResourceExists(pagesDir) ? await walkDirectoryForPages(pagesDir) : graph;

const has404Page = graph.filter(page => page.route === '/404/').length === 1;
const has404Page = graph.find(page => page.route.endsWith('/404/'));

// if the _only_ page is a 404 page, still provide a default index.html
if (has404Page && graph.length === 1) {
Expand All @@ -293,9 +294,9 @@ const generateGraph = async (compilation) => {
...graph,
{
...oldGraph,
outputPath: '404.html',
outputPath: '/404.html',
filename: '404.html',
route: '/404/',
route: `${basePath}/404/`,
path: '404.html',
id: '404',
label: 'Not Found'
Expand Down
17 changes: 7 additions & 10 deletions packages/cli/src/lifecycles/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { WorkerPool } from '../lib/threadpool.js';
// TODO a lot of these are duplicated in the build lifecycle too
// would be good to refactor
async function createOutputDirectory(route, outputDir) {
if (route !== '/404/' && !await checkResourceExists(outputDir)) {
if (!route.endsWith('/404/') && !await checkResourceExists(outputDir)) {
await fs.mkdir(outputDir, {
recursive: true
});
Expand Down Expand Up @@ -59,15 +59,14 @@ async function preRenderCompilationWorker(compilation, workerPrerender) {

for (const page of pages) {
const { route, outputPath, resources } = page;
const outputDirUrl = new URL(`./${route}/`, scratchDir);
const outputPathUrl = new URL(`./${outputPath}`, scratchDir);
const outputPathUrl = new URL(`.${outputPath}`, scratchDir);
const url = new URL(`http://localhost:${compilation.config.port}${route}`);
const request = new Request(url);

let body = await (await servePage(url, request, plugins)).text();
body = await (await interceptPage(url, request, plugins, body)).text();

await createOutputDirectory(route, outputDirUrl);
await createOutputDirectory(route, new URL(outputPathUrl.href.replace('index.html', '')));

const scripts = resources
.map(resource => compilation.resources.get(resource))
Expand Down Expand Up @@ -106,8 +105,7 @@ async function preRenderCompilationCustom(compilation, customPrerender) {

await renderer(compilation, async (page, body) => {
const { route, outputPath } = page;
const outputDirUrl = new URL(`./${route}`, scratchDir);
const outputPathUrl = new URL(`./${outputPath}`, scratchDir);
const outputPathUrl = new URL(`.${outputPath}`, scratchDir);

// clean up special Greenwood dev only assets that would come through if prerendering with a headless browser
body = body.replace(/<script src="(.*lit\/polyfill-support.js)"><\/script>/, '');
Expand All @@ -119,7 +117,7 @@ async function preRenderCompilationCustom(compilation, customPrerender) {
body = body.replace(/<script src="(.*webcomponents-bundle.js)"><\/script>/, '');

await trackResourcesForRoute(body, compilation, route);
await createOutputDirectory(route, outputDirUrl);
await createOutputDirectory(route, new URL(outputPathUrl.href.replace('index.html', '')));
await fs.writeFile(outputPathUrl, body);

console.info('generated page...', route);
Expand All @@ -135,15 +133,14 @@ async function staticRenderCompilation(compilation) {

await Promise.all(pages.map(async (page) => {
const { route, outputPath } = page;
const outputDirUrl = new URL(`.${route}`, scratchDir);
const outputPathUrl = new URL(`./${outputPath}`, scratchDir);
const outputPathUrl = new URL(`.${outputPath}`, scratchDir);
const url = new URL(`http://localhost:${compilation.config.port}${route}`);
const request = new Request(url);

let body = await (await servePage(url, request, plugins)).text();
body = await (await interceptPage(url, request, plugins, body)).text();

await createOutputDirectory(route, outputDirUrl);
await createOutputDirectory(route, new URL(outputPathUrl.href.replace('index.html', '')));
await fs.writeFile(outputPathUrl, body);

console.info('generated page...', route);
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/lifecycles/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ async function getDevServer(compilation) {
async function getStaticServer(compilation, composable) {
const app = new Koa();
const { outputDir } = compilation.context;
const { port } = compilation.config;
const { port, basePath } = compilation.config;
const standardResourcePlugins = compilation.config.plugins.filter((plugin) => {
return plugin.type === 'resource' && plugin.isGreenwoodDefaultPlugin;
});
Expand All @@ -186,7 +186,7 @@ async function getStaticServer(compilation, composable) {
? 'index.html'
: isStatic
? matchingRoute.outputPath
: url.pathname;
: url.pathname.replace(basePath, '');
const body = await fs.readFile(new URL(`./${pathname}`, outputDir), 'utf-8');

ctx.set('Content-Type', 'text/html');
Expand Down Expand Up @@ -235,7 +235,7 @@ async function getStaticServer(compilation, composable) {

app.use(async (ctx, next) => {
try {
const url = new URL(`.${ctx.url}`, outputDir.href);
const url = new URL(`.${ctx.url.replace(basePath, '')}`, outputDir.href);

if (await checkResourceExists(url)) {
const resourcePlugins = standardResourcePlugins
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/plugins/resource/plugin-dev-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ class DevProxyResource extends ResourceInterface {

async serve(url, request) {
const { pathname } = url;
const proxies = this.compilation.config.devServer.proxy;
const { config } = this.compilation;
const proxies = config.devServer.proxy;
const { basePath } = config;
const proxyBaseUrl = Object.entries(proxies).reduce((acc, entry) => {
return pathname.indexOf(entry[0]) >= 0
? `${entry[1]}${pathname}`
return pathname.indexOf(`${basePath}${entry[0]}`) >= 0
? `${entry[1]}${pathname.replace(basePath, '')}`
: acc;
}, pathname);
const requestProxied = new Request(`${proxyBaseUrl}${url.search}`, {
Expand Down
14 changes: 7 additions & 7 deletions packages/cli/src/plugins/resource/plugin-standard-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class StandardHtmlResource extends ResourceInterface {
}

body = await getAppTemplate(body, context, customImports, contextPlugins, config.devServer.hud, title);
body = await getUserScripts(body, context);
body = await getUserScripts(body, this.compilation);

if (processedMarkdown) {
const wrappedCustomElementRegex = /<p><[a-zA-Z]*-[a-zA-Z](.*)>(.*)<\/[a-zA-Z]*-[a-zA-Z](.*)><\/p>/g;
Expand Down Expand Up @@ -220,7 +220,7 @@ class StandardHtmlResource extends ResourceInterface {
}

async optimize(url, response) {
const { optimization } = this.compilation.config;
const { optimization, basePath } = this.compilation.config;
const { pathname } = url;
const pageResources = this.compilation.graph.find(page => page.outputPath === pathname || page.route === pathname).resources;
let body = await response.text();
Expand All @@ -232,7 +232,7 @@ class StandardHtmlResource extends ResourceInterface {
if (src) {
if (type === 'script') {
if (!optimizationAttr && optimization === 'default') {
const optimizedFilePath = `/${optimizedFileName}`;
const optimizedFilePath = `${basePath}/${optimizedFileName}`;

body = body.replace(src, optimizedFilePath);
body = body.replace('<head>', `
Expand All @@ -244,15 +244,15 @@ class StandardHtmlResource extends ResourceInterface {

body = body.replace(`<script ${rawAttributes}></script>`, `
<script ${isModule}>
${optimizedFileContents.replace(/\.\//g, '/').replace(/\$/g, '$$$')}
${optimizedFileContents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$')}
</script>
`);
} else if (optimizationAttr === 'static' || optimization === 'static') {
body = body.replace(`<script ${rawAttributes}></script>`, '');
}
} else if (type === 'link') {
if (!optimizationAttr && (optimization !== 'none' && optimization !== 'inline')) {
const optimizedFilePath = `/${optimizedFileName}`;
const optimizedFilePath = `${basePath}/${optimizedFileName}`;

body = body.replace(src, optimizedFilePath);
body = body.replace('<head>', `
Expand Down Expand Up @@ -280,9 +280,9 @@ class StandardHtmlResource extends ResourceInterface {
if (optimizationAttr === 'static' || optimization === 'static') {
body = body.replace(`<script ${rawAttributes}>${contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')}</script>`, '');
} else if (optimizationAttr === 'none') {
body = body.replace(contents, contents.replace(/\.\//g, '/').replace(/\$/g, '$$$'));
body = body.replace(contents, contents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$'));
} else {
body = body.replace(contents, optimizedFileContents.replace(/\.\//g, '/').replace(/\$/g, '$$$'));
body = body.replace(contents, optimizedFileContents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$'));
}
} else if (type === 'style') {
body = body.replace(contents, optimizedFileContents);
Expand Down
24 changes: 13 additions & 11 deletions packages/cli/src/plugins/resource/plugin-static-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,30 +56,31 @@ class StaticRouterResource extends ResourceInterface {

async optimize(url, response) {
let body = await response.text();
const { basePath } = this.compilation.config;
const { pathname } = url;
const isStaticRoute = this.compilation.graph.find(page => page.route === pathname && !page.isSSR);
const { outputDir } = this.compilation.context;
const partial = body.match(/<body>(.*)<\/body>/s)[0].replace('<body>', '').replace('</body>', '');
const outputPartialDirUrl = new URL(`./_routes${url.pathname}`, outputDir);
const outputPartialDirUrl = new URL(`./_routes${url.pathname.replace(basePath, '')}`, outputDir);
const outputPartialDirPathUrl = new URL(`file://${outputPartialDirUrl.pathname.split('/').slice(0, -1).join('/').concat('/')}`);
let currentTemplate;

const routeTags = this.compilation.graph
.filter(page => !page.isSSR)
.filter(page => page.route !== '/404/')
.filter(page => !page.route.endsWith('/404/'))
.map((page) => {
const template = page.filename && page.filename.split('.').pop() === this.extensions[0]
? page.route
: page.template;
const key = page.route === '/'
? ''
: page.route.slice(0, page.route.lastIndexOf('/'));
: page.route.slice(0, page.route.lastIndexOf('/')).replace(basePath, '');

if (pathname === page.route) {
currentTemplate = template;
}
return `
<greenwood-route data-route="${page.route}" data-template="${template}" data-key="/_routes${key}/index.html"></greenwood-route>
<greenwood-route data-route="${page.route}" data-template="${template}" data-key="${basePath}/_routes${key}/index.html"></greenwood-route>
`;
});

Expand All @@ -93,13 +94,14 @@ class StaticRouterResource extends ResourceInterface {
await fs.writeFile(new URL('./index.html', outputPartialDirUrl), partial);
}

body = body.replace('</head>', `
<script>
window.__greenwood = window.__greenwood || {};
window.__greenwood.currentTemplate = "${currentTemplate}";
</script>
</head>
`.replace(/\n/g, '').replace(/ /g, ''))
body = body
.replace('</head>', `
<script data-gwd="static-router">
window.__greenwood = window.__greenwood || {};
window.__greenwood.currentTemplate = "${currentTemplate}";
</script>
</head>
`)
.replace(/<body>(.*)<\/body>/s, `
<body>\n
Expand Down
Loading

0 comments on commit d2a6103

Please sign in to comment.