@greenwood/cli
Advanced tools
Comparing version 0.28.0-alpha.4 to 0.28.0-alpha.5
{ | ||
"name": "@greenwood/cli", | ||
"version": "0.28.0-alpha.4", | ||
"version": "0.28.0-alpha.5", | ||
"description": "Greenwood CLI.", | ||
@@ -28,5 +28,7 @@ "type": "module", | ||
"dependencies": { | ||
"@rollup/plugin-commonjs": "^21.0.0", | ||
"@rollup/plugin-node-resolve": "^13.0.0", | ||
"@rollup/plugin-replace": "^2.3.4", | ||
"@rollup/plugin-terser": "^0.1.0", | ||
"@web/rollup-plugin-import-meta-assets": "^1.0.0", | ||
"acorn": "^8.0.1", | ||
@@ -49,3 +51,3 @@ "acorn-walk": "^8.0.0", | ||
"unified": "^9.2.0", | ||
"wc-compiler": "~0.6.1" | ||
"wc-compiler": "~0.8.0" | ||
}, | ||
@@ -71,3 +73,3 @@ "devDependencies": { | ||
}, | ||
"gitHead": "93ed8d8628c371e8cb46c503cad2190b0fa8e1ee" | ||
"gitHead": "5711324b17929859a894cd7798dfd41ba1e26866" | ||
} |
import fs from 'fs/promises'; | ||
import { checkResourceExists, normalizePathnameForWindows } from '../lib/resource-utils.js'; | ||
import { nodeResolve } from '@rollup/plugin-node-resolve'; | ||
import commonjs from '@rollup/plugin-commonjs'; | ||
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets'; | ||
// specifically to handle escodegen using require for package.json | ||
// https://github.com/estools/escodegen/issues/455 | ||
function greenwoodJsonLoader() { | ||
return { | ||
name: 'greenwood-json-loader', | ||
async load(id) { | ||
const extension = id.split('.').pop(); | ||
if (extension === 'json') { | ||
const url = new URL(`file://${id}`); | ||
const json = JSON.parse(await fs.readFile(url, 'utf-8')); | ||
const contents = `export default ${JSON.stringify(json)}`; | ||
return contents; | ||
} | ||
} | ||
}; | ||
} | ||
function greenwoodResourceLoader (compilation) { | ||
@@ -111,3 +133,3 @@ const resourcePlugins = compilation.config.plugins.filter((plugin) => { | ||
const getRollupConfig = async (compilation) => { | ||
const getRollupConfigForScriptResources = async (compilation) => { | ||
const { outputDir } = compilation.context; | ||
@@ -169,2 +191,50 @@ const input = [...compilation.resources.values()] | ||
export { getRollupConfig }; | ||
const getRollupConfigForApis = async (compilation) => { | ||
const { outputDir, userWorkspace } = compilation.context; | ||
const input = [...compilation.manifest.apis.values()] | ||
.map(api => normalizePathnameForWindows(new URL(`.${api.path}`, userWorkspace))); | ||
// TODO should routes and APIs have chunks? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
return [{ | ||
input, | ||
output: { | ||
dir: `${normalizePathnameForWindows(outputDir)}/api`, | ||
entryFileNames: '[name].js', | ||
chunkFileNames: '[name].[hash].js' | ||
}, | ||
plugins: [ | ||
greenwoodJsonLoader(), | ||
nodeResolve(), | ||
commonjs(), | ||
importMetaAssets() | ||
] | ||
}]; | ||
}; | ||
const getRollupConfigForSsr = async (compilation, input) => { | ||
const { outputDir } = compilation.context; | ||
// TODO should routes and APIs have chunks? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
return [{ | ||
input, | ||
output: { | ||
dir: normalizePathnameForWindows(outputDir), | ||
entryFileNames: '_[name].js', | ||
chunkFileNames: '[name].[hash].js' | ||
}, | ||
plugins: [ | ||
greenwoodJsonLoader(), | ||
nodeResolve(), | ||
commonjs(), | ||
importMetaAssets() | ||
] | ||
}]; | ||
}; | ||
export { | ||
getRollupConfigForApis, | ||
getRollupConfigForScriptResources, | ||
getRollupConfigForSsr | ||
}; |
@@ -7,3 +7,2 @@ #!/usr/bin/env node | ||
import program from 'commander'; | ||
import { URL } from 'url'; | ||
@@ -60,2 +59,3 @@ const greenwoodPackageJson = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')); | ||
const run = async() => { | ||
process.env.__GWD_COMMAND__ = command; | ||
const compilation = await generateCompilation(); | ||
@@ -65,3 +65,2 @@ | ||
console.info(`Running Greenwood with the ${command} command.`); | ||
process.env.__GWD_COMMAND__ = command; | ||
@@ -79,5 +78,2 @@ switch (command) { | ||
case 'serve': | ||
process.env.__GWD_COMMAND__ = 'build'; | ||
await (await import('./commands/build.js')).runProductionBuild(compilation); | ||
await (await import('./commands/serve.js')).runProdServer(compilation); | ||
@@ -84,0 +80,0 @@ |
// TODO convert this to use / return URLs | ||
// https://github.com/ProjectEvergreen/greenwood/issues/953 | ||
import { createRequire } from 'module'; // https://stackoverflow.com/a/62499498/417806 | ||
import { checkResourceExists } from '../lib/resource-utils.js'; | ||
import { checkResourceExists } from './resource-utils.js'; | ||
import fs from 'fs/promises'; | ||
@@ -6,0 +6,0 @@ |
import fs from 'fs/promises'; | ||
import { hashString } from '../lib/hashing-utils.js'; | ||
import { hashString } from './hashing-utils.js'; | ||
@@ -57,3 +57,3 @@ async function modelResource(context, type, src = undefined, contents = undefined, optimizationAttr = undefined, rawAttributes = undefined) { | ||
// On Windows, a URL with a drive letter like C:/ thinks it is a protocol and so prepends a /, e.g. /C:/ | ||
// This is fine with never fs methods that Greenwood uses, but tools like Rollupand PostCSS will need this handled manually | ||
// This is fine with never fs methods that Greenwood uses, but tools like Rollup and PostCSS will need this handled manually | ||
// https://github.com/rollup/rollup/issues/3779 | ||
@@ -60,0 +60,0 @@ function normalizePathnameForWindows(url) { |
@@ -1,9 +0,28 @@ | ||
/* eslint-disable max-depth */ | ||
/* eslint-disable max-depth, max-len */ | ||
import fs from 'fs/promises'; | ||
import { getRollupConfig } from '../config/rollup.config.js'; | ||
import { getRollupConfigForApis, getRollupConfigForScriptResources, getRollupConfigForSsr } from '../config/rollup.config.js'; | ||
import { hashString } from '../lib/hashing-utils.js'; | ||
import { checkResourceExists, mergeResponse } from '../lib/resource-utils.js'; | ||
import { checkResourceExists, mergeResponse, normalizePathnameForWindows } from '../lib/resource-utils.js'; | ||
import path from 'path'; | ||
import { rollup } from 'rollup'; | ||
async function emitResources(compilation) { | ||
const { outputDir } = compilation.context; | ||
const { resources } = compilation; | ||
// https://stackoverflow.com/a/56150320/417806 | ||
// TODO put into a util | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
await fs.writeFile(new URL('./resources.json', outputDir), JSON.stringify(resources, (key, value) => { | ||
if (value instanceof Map) { | ||
return { | ||
dataType: 'Map', | ||
value: [...value] | ||
}; | ||
} else { | ||
return value; | ||
} | ||
})); | ||
} | ||
async function cleanUpResources(compilation) { | ||
@@ -144,5 +163,126 @@ const { outputDir } = compilation.context; | ||
async function bundleApiRoutes(compilation) { | ||
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api | ||
const [rollupConfig] = await getRollupConfigForApis(compilation); | ||
if (rollupConfig.input.length !== 0) { | ||
const bundle = await rollup(rollupConfig); | ||
await bundle.write(rollupConfig.output); | ||
} | ||
} | ||
async function bundleSsrPages(compilation) { | ||
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api | ||
const { outputDir, pagesDir } = compilation.context; | ||
// TODO context plugins for SSR ? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
// const contextPlugins = compilation.config.plugins.filter((plugin) => { | ||
// return plugin.type === 'context'; | ||
// }).map((plugin) => { | ||
// return plugin.provider(compilation); | ||
// }); | ||
const input = []; | ||
// TODO ideally be able to serialize entire graph (or only an explicit subset?) | ||
// right now page.imports is breaking JSON.stringify | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
const intermediateGraph = compilation.graph.map(page => { | ||
const p = { ...page }; | ||
delete p.imports; | ||
return p; | ||
}); | ||
for (const page of compilation.graph) { | ||
if (page.isSSR && !page.data.static) { | ||
const { filename, path: pagePath } = page; | ||
const scratchUrl = new URL(`./${filename}`, outputDir); | ||
// better way to write out inline code like this? | ||
await fs.writeFile(scratchUrl, ` | ||
import { Worker } from 'worker_threads'; | ||
import { getAppTemplate, getPageTemplate, getUserScripts } from '@greenwood/cli/src/lib/templating-utils.js'; | ||
export async function handler(request, compilation) { | ||
const routeModuleLocationUrl = new URL('./_${filename}', '${outputDir}'); | ||
const routeWorkerUrl = '${compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().workerUrl}'; | ||
const htmlOptimizer = compilation.config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation); | ||
let body = 'Hello from the ${page.id} page!'; | ||
let html = ''; | ||
let frontmatter; | ||
let template; | ||
let templateType = 'page'; | ||
let title = ''; | ||
let imports = []; | ||
await new Promise((resolve, reject) => { | ||
const worker = new Worker(new URL(routeWorkerUrl)); | ||
worker.on('message', (result) => { | ||
if (result.body) { | ||
body = result.body; | ||
} | ||
if (result.template) { | ||
template = result.template; | ||
} | ||
if (result.frontmatter) { | ||
frontmatter = result.frontmatter; | ||
if (frontmatter.title) { | ||
title = frontmatter.title; | ||
} | ||
if (frontmatter.template) { | ||
templateType = frontmatter.template; | ||
} | ||
if (frontmatter.imports) { | ||
imports = imports.concat(frontmatter.imports); | ||
} | ||
} | ||
resolve(); | ||
}); | ||
worker.on('error', reject); | ||
worker.on('exit', (code) => { | ||
if (code !== 0) { | ||
reject(new Error(\`Worker stopped with exit code \${code}\`)); | ||
} | ||
}); | ||
worker.postMessage({ | ||
moduleUrl: routeModuleLocationUrl.href, | ||
compilation: \`${JSON.stringify({ graph: intermediateGraph })}\`, | ||
route: '${pagePath}' | ||
}); | ||
}); | ||
html = template ? template : await getPageTemplate('', compilation.context, templateType, []); | ||
html = await getAppTemplate(html, compilation.context, imports, [], false, title); | ||
html = await getUserScripts(html, compilation.context); | ||
html = html.replace(\/\<content-outlet>(.*)<\\/content-outlet>\/s, body); | ||
html = await (await htmlOptimizer.optimize(new URL(request.url), new Response(html))).text(); | ||
return new Response(html); | ||
} | ||
`); | ||
input.push(normalizePathnameForWindows(new URL(`./${filename}`, pagesDir))); | ||
} | ||
} | ||
const [rollupConfig] = await getRollupConfigForSsr(compilation, input); | ||
if (rollupConfig.input.length !== 0) { | ||
const bundle = await rollup(rollupConfig); | ||
await bundle.write(rollupConfig.output); | ||
} | ||
} | ||
async function bundleScriptResources(compilation) { | ||
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api | ||
const [rollupConfig] = await getRollupConfig(compilation); | ||
const [rollupConfig] = await getRollupConfigForScriptResources(compilation); | ||
@@ -177,2 +317,4 @@ if (rollupConfig.input.length !== 0) { | ||
await Promise.all([ | ||
await bundleApiRoutes(compilation), | ||
await bundleSsrPages(compilation), | ||
await bundleScriptResources(compilation), | ||
@@ -186,2 +328,3 @@ await bundleStyleResources(compilation, optimizeResourcePlugins) | ||
await cleanUpResources(compilation); | ||
await emitResources(compilation); | ||
@@ -188,0 +331,0 @@ resolve(); |
@@ -0,4 +1,6 @@ | ||
import { checkResourceExists } from '../lib/resource-utils.js'; | ||
import { generateGraph } from './graph.js'; | ||
import { initContext } from './context.js'; | ||
import fs from 'fs/promises'; | ||
import { readAndMergeConfig as initConfig } from './config.js'; | ||
import { initContext } from './context.js'; | ||
import { generateGraph } from './graph.js'; | ||
@@ -13,3 +15,8 @@ const generateCompilation = () => { | ||
config: {}, | ||
resources: new Map() | ||
// TODO put resources into manifest | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
resources: new Map(), | ||
manifest: { | ||
apis: new Map() | ||
} | ||
}; | ||
@@ -23,7 +30,85 @@ | ||
compilation.context = await initContext(compilation); | ||
// generate a graph of all pages / components to build | ||
console.info('Generating graph of workspace files...'); | ||
compilation = await generateGraph(compilation); | ||
const { scratchDir, outputDir } = compilation.context; | ||
if (!await checkResourceExists(scratchDir)) { | ||
await fs.mkdir(scratchDir); | ||
} | ||
if (process.env.__GWD_COMMAND__ === 'serve') { // eslint-disable-line no-underscore-dangle | ||
console.info('Loading graph from build output...'); | ||
if (!await checkResourceExists(new URL('./graph.json', outputDir))) { | ||
reject(new Error('No build output detected. Make sure you have run greenwood build')); | ||
} | ||
compilation.graph = JSON.parse(await fs.readFile(new URL('./graph.json', outputDir), 'utf-8')); | ||
// hydrate URLs | ||
compilation.graph.forEach((page, idx) => { | ||
if (page.imports.length > 0) { | ||
page.imports.forEach((imp, jdx) => { | ||
compilation.graph[idx].imports[jdx].sourcePathURL = new URL(imp.sourcePathURL); | ||
}); | ||
} | ||
}); | ||
if (await checkResourceExists(new URL('./manifest.json', outputDir))) { | ||
console.info('Loading manifest from build output...'); | ||
// TODO put reviver into a utility? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
const manifest = JSON.parse(await fs.readFile(new URL('./manifest.json', outputDir)), function reviver(key, value) { | ||
if (typeof value === 'object' && value !== null) { | ||
if (value.dataType === 'Map') { | ||
return new Map(value.value); | ||
} | ||
} | ||
return value; | ||
}); | ||
compilation.manifest = manifest; | ||
} | ||
if (await checkResourceExists(new URL('./resources.json', outputDir))) { | ||
console.info('Loading resources from build output...'); | ||
// TODO put reviver into a utility? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
const resources = JSON.parse(await fs.readFile(new URL('./resources.json', outputDir)), function reviver(key, value) { | ||
if (typeof value === 'object' && value !== null) { | ||
if (value.dataType === 'Map') { | ||
// revive URLs | ||
if (value.value.sourcePathURL) { | ||
value.value.sourcePathURL = new URL(value.value.sourcePathURL); | ||
} | ||
return new Map(value.value); | ||
} | ||
} | ||
return value; | ||
}); | ||
compilation.resources = resources; | ||
} | ||
} else { | ||
// generate a graph of all pages / components to build | ||
console.info('Generating graph of workspace files...'); | ||
compilation = await generateGraph(compilation); | ||
// https://stackoverflow.com/a/56150320/417806 | ||
// TODO put reviver into a util? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
await fs.writeFile(new URL('./manifest.json', scratchDir), JSON.stringify(compilation.manifest, (key, value) => { | ||
if (value instanceof Map) { | ||
return { | ||
dataType: 'Map', | ||
value: [...value] | ||
}; | ||
} else { | ||
return value; | ||
} | ||
})); | ||
await fs.writeFile(new URL('./graph.json', scratchDir), JSON.stringify(compilation.graph)); | ||
} | ||
resolve(compilation); | ||
@@ -30,0 +115,0 @@ } catch (err) { |
@@ -28,4 +28,7 @@ import fs from 'fs/promises'; | ||
}))).flat(PLUGINS_FLATTENED_DEPTH).map((plugin) => { | ||
const isStandardStaticResource = (plugin.name.startsWith('plugin-standard') && plugin.name !== 'plugin-standard-html') || plugin.name === 'plugin-source-maps'; | ||
return { | ||
isGreenwoodDefaultPlugin: true, | ||
isStandardStaticResource, | ||
...plugin | ||
@@ -32,0 +35,0 @@ }; |
@@ -14,2 +14,3 @@ import fs from 'fs/promises'; | ||
const dataDir = new URL('../data/', import.meta.url); | ||
const templatesDir = new URL('../templates/', import.meta.url); | ||
const userWorkspace = workspace; | ||
@@ -19,2 +20,3 @@ const apisDir = new URL('./api/', userWorkspace); | ||
const userTemplatesDir = new URL(`./${templatesDirectory}/`, userWorkspace); | ||
const context = { | ||
@@ -28,3 +30,4 @@ dataDir, | ||
scratchDir, | ||
projectDirectory | ||
projectDirectory, | ||
templatesDir | ||
}; | ||
@@ -31,0 +34,0 @@ |
@@ -13,3 +13,3 @@ /* eslint-disable complexity, max-depth */ | ||
const { context } = compilation; | ||
const { pagesDir, projectDirectory, userWorkspace, scratchDir } = context; | ||
const { apisDir, pagesDir, projectDirectory, userWorkspace } = context; | ||
let graph = [{ | ||
@@ -213,3 +213,43 @@ outputPath: 'index.html', | ||
const walkDirectoryForApis = async function(directory, apis = new Map()) { | ||
const files = await fs.readdir(directory); | ||
for (const filename of files) { | ||
const filenameUrl = new URL(`./${filename}`, directory); | ||
const filenameUrlAsDir = new URL(`./${filename}/`, directory); | ||
const isDirectory = await checkResourceExists(filenameUrlAsDir) && (await fs.stat(filenameUrlAsDir)).isDirectory(); | ||
if (isDirectory) { | ||
apis = await walkDirectoryForApis(filenameUrlAsDir, apis); | ||
} else { | ||
const extension = filenameUrl.pathname.split('.').pop(); | ||
const relativeApiPath = filenameUrl.pathname.replace(userWorkspace.pathname, '/'); | ||
const route = relativeApiPath.replace(`.${extension}`, ''); | ||
if (extension !== 'js') { | ||
console.warn(`${filenameUrl} is not a JavaScript file, skipping...`); | ||
} else { | ||
/* | ||
* API Properties (per route) | ||
*---------------------- | ||
* filename: base filename of the page | ||
* outputPath: the filename to write to when generating a build | ||
* path: path to the file relative to the workspace | ||
* route: URL route for a given page on outputFilePath | ||
*/ | ||
apis.set(route, { | ||
filename: filename, | ||
outputPath: `/api/${filename}`, | ||
path: relativeApiPath, | ||
route | ||
}); | ||
} | ||
} | ||
} | ||
return apis; | ||
}; | ||
console.debug('building from local sources...'); | ||
// test for SPA | ||
@@ -281,8 +321,8 @@ if (await checkResourceExists(new URL('./index.html', userWorkspace))) { | ||
if (!await checkResourceExists(scratchDir)) { | ||
await fs.mkdir(scratchDir); | ||
if (await checkResourceExists(apisDir)) { | ||
const apis = await walkDirectoryForApis(apisDir); | ||
compilation.manifest = { apis }; | ||
} | ||
await fs.writeFile(new URL('./graph.json', scratchDir), JSON.stringify(compilation.graph)); | ||
resolve(compilation); | ||
@@ -289,0 +329,0 @@ } catch (err) { |
@@ -72,2 +72,3 @@ import fs from 'fs/promises'; | ||
response = merged; | ||
break; | ||
} | ||
@@ -168,5 +169,3 @@ } | ||
const standardResourcePlugins = compilation.config.plugins.filter((plugin) => { | ||
return plugin.type === 'resource' | ||
&& plugin.isGreenwoodDefaultPlugin | ||
&& plugin.name !== 'plugin-standard-html'; | ||
return plugin.type === 'resource' && plugin.isGreenwoodDefaultPlugin; | ||
}); | ||
@@ -232,5 +231,7 @@ | ||
const url = new URL(`.${ctx.url}`, outputDir.href); | ||
const resourcePlugins = standardResourcePlugins.map((plugin) => { | ||
return plugin.provider(compilation); | ||
}); | ||
const resourcePlugins = standardResourcePlugins | ||
.filter((plugin) => plugin.isStandardStaticResource) | ||
.map((plugin) => { | ||
return plugin.provider(compilation); | ||
}); | ||
@@ -275,6 +276,5 @@ const request = new Request(url.href, { | ||
async function getHybridServer(compilation) { | ||
const { graph, manifest, context } = compilation; | ||
const { outputDir } = context; | ||
const app = await getStaticServer(compilation, true); | ||
const resourcePlugins = compilation.config.plugins.filter((plugin) => { | ||
return plugin.type === 'resource'; | ||
}); | ||
@@ -284,3 +284,4 @@ app.use(async (ctx) => { | ||
const url = new URL(`http://localhost:8080${ctx.url}`); | ||
const matchingRoute = compilation.graph.find((node) => node.route === url.pathname) || { data: {} }; | ||
const matchingRoute = graph.find((node) => node.route === url.pathname) || { data: {} }; | ||
const isApiRoute = manifest.apis.has(url.pathname); | ||
const request = new Request(url.href, { | ||
@@ -290,17 +291,9 @@ method: ctx.request.method, | ||
}); | ||
const apiResource = resourcePlugins.find((plugin) => { | ||
return plugin.isGreenwoodDefaultPlugin | ||
&& plugin.name === 'plugin-api-routes'; | ||
}).provider(compilation); | ||
const isApiRoute = await apiResource.shouldServe(url, request); | ||
if (matchingRoute.isSSR && !matchingRoute.data.static) { | ||
const standardHtmlResource = resourcePlugins.find((plugin) => { | ||
return plugin.isGreenwoodDefaultPlugin | ||
&& plugin.name.indexOf('plugin-standard-html') === 0; | ||
}).provider(compilation); | ||
let response = await standardHtmlResource.serve(url, request); | ||
const { handler } = await import(new URL(`./${matchingRoute.filename}`, outputDir)); | ||
// TODO passing compilation this way too hacky? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1008 | ||
const response = await handler(request, compilation); | ||
response = await standardHtmlResource.optimize(url, response); | ||
ctx.body = Readable.from(response.body); | ||
@@ -310,3 +303,5 @@ ctx.set('Content-Type', 'text/html'); | ||
} else if (isApiRoute) { | ||
const response = await apiResource.serve(url, request); | ||
const apiRoute = manifest.apis.get(url.pathname); | ||
const { handler } = await import(new URL(`.${apiRoute.path}`, outputDir)); | ||
const response = await handler(request); | ||
@@ -313,0 +308,0 @@ ctx.status = 200; |
@@ -6,3 +6,2 @@ /* | ||
*/ | ||
import { checkResourceExists } from '../../lib/resource-utils.js'; | ||
import { ResourceInterface } from '../../lib/resource-interface.js'; | ||
@@ -17,16 +16,13 @@ | ||
const { protocol, pathname } = url; | ||
const apiPathUrl = new URL(`.${pathname.replace('/api', '')}.js`, this.compilation.context.apisDir); | ||
if (protocol.startsWith('http') && pathname.startsWith('/api') && await checkResourceExists(apiPathUrl)) { | ||
return true; | ||
} | ||
return protocol.startsWith('http') && this.compilation.manifest.apis.has(pathname); | ||
} | ||
async serve(url, request) { | ||
let href = new URL(`./${url.pathname.replace('/api/', '')}.js`, `file://${this.compilation.context.apisDir.pathname}`).href; | ||
const api = this.compilation.manifest.apis.get(url.pathname); | ||
const apiUrl = new URL(`.${api.path}`, this.compilation.context.userWorkspace); | ||
// https://github.com/nodejs/modules/issues/307#issuecomment-1165387383 | ||
if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle | ||
href = `${href}?t=${Date.now()}`; | ||
} | ||
const href = process.env.__GWD_COMMAND__ === 'develop' // eslint-disable-line no-underscore-dangle | ||
? `${apiUrl.href}?t=${Date.now()}` | ||
: apiUrl.href; | ||
@@ -33,0 +29,0 @@ const { handler } = await import(href); |
@@ -8,8 +8,4 @@ /* eslint-disable complexity, max-depth */ | ||
*/ | ||
import { checkResourceExists } from '../../lib/resource-utils.js'; | ||
import frontmatter from 'front-matter'; | ||
import fs from 'fs/promises'; | ||
import { getPackageJson } from '../../lib/node-modules-utils.js'; | ||
import htmlparser from 'node-html-parser'; | ||
import path from 'path'; | ||
import rehypeStringify from 'rehype-stringify'; | ||
@@ -21,188 +17,6 @@ import rehypeRaw from 'rehype-raw'; | ||
import { ResourceInterface } from '../../lib/resource-interface.js'; | ||
import { getUserScripts, getPageTemplate, getAppTemplate } from '../../lib/templating-utils.js'; | ||
import unified from 'unified'; | ||
import { Worker } from 'worker_threads'; | ||
async function getCustomPageTemplatesFromPlugins(contextPlugins, templateName) { | ||
const customTemplateLocations = []; | ||
const templateDir = contextPlugins | ||
.map(plugin => plugin.templates) | ||
.flat(); | ||
for (const templateDirUrl of templateDir) { | ||
if (templateName) { | ||
const templateUrl = new URL(`./${templateName}.html`, templateDirUrl); | ||
if (await checkResourceExists(templateUrl)) { | ||
customTemplateLocations.push(templateUrl); | ||
} | ||
} | ||
} | ||
return customTemplateLocations; | ||
} | ||
const getPageTemplate = async (filePath, { userTemplatesDir, pagesDir, projectDirectory }, template, contextPlugins = []) => { | ||
const customPluginDefaultPageTemplates = await getCustomPageTemplatesFromPlugins(contextPlugins, 'page'); | ||
const customPluginPageTemplates = await getCustomPageTemplatesFromPlugins(contextPlugins, template); | ||
const extension = filePath.split('.').pop(); | ||
const is404Page = filePath.startsWith('404') && extension === 'html'; | ||
const hasCustomTemplate = await checkResourceExists(new URL(`./${template}.html`, userTemplatesDir)); | ||
const hasPageTemplate = await checkResourceExists(new URL('./page.html', userTemplatesDir)); | ||
const hasCustom404Page = await checkResourceExists(new URL('./404.html', pagesDir)); | ||
const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(`./${filePath}`, projectDirectory)); | ||
let contents; | ||
if (template && (customPluginPageTemplates.length > 0 || hasCustomTemplate)) { | ||
// use a custom template, usually from markdown frontmatter | ||
contents = customPluginPageTemplates.length > 0 | ||
? await fs.readFile(new URL(`./${template}.html`, customPluginPageTemplates[0]), 'utf-8') | ||
: await fs.readFile(new URL(`./${template}.html`, userTemplatesDir), 'utf-8'); | ||
} else if (isHtmlPage) { | ||
// if the page is already HTML, use that as the template, NOT accounting for 404 pages | ||
contents = await fs.readFile(new URL(`./${filePath}`, projectDirectory), 'utf-8'); | ||
} else if (customPluginDefaultPageTemplates.length > 0 || (!is404Page && hasPageTemplate)) { | ||
// else look for default page template from the user | ||
// and 404 pages should be their own "top level" template | ||
contents = customPluginDefaultPageTemplates.length > 0 | ||
? await fs.readFile(new URL('./page.html', customPluginDefaultPageTemplates[0]), 'utf-8') | ||
: await fs.readFile(new URL('./page.html', userTemplatesDir), 'utf-8'); | ||
} else if (is404Page && !hasCustom404Page) { | ||
contents = await fs.readFile(new URL('../../templates/404.html', import.meta.url), 'utf-8'); | ||
} else { | ||
// fallback to using Greenwood's stock page template | ||
contents = await fs.readFile(new URL('../../templates/page.html', import.meta.url), 'utf-8'); | ||
} | ||
return contents; | ||
}; | ||
const getAppTemplate = async (pageTemplateContents, templatesDir, customImports = [], contextPlugins, enableHud, frontmatterTitle) => { | ||
const userAppTemplateUrl = new URL('./app.html', templatesDir); | ||
const customAppTemplatesFromPlugins = await getCustomPageTemplatesFromPlugins(contextPlugins, 'app'); | ||
const hasCustomUserAppTemplate = await checkResourceExists(userAppTemplateUrl); | ||
let appTemplateContents = customAppTemplatesFromPlugins.length > 0 | ||
? await fs.readFile(new URL('./app.html', customAppTemplatesFromPlugins[0])) | ||
: hasCustomUserAppTemplate | ||
? await fs.readFile(userAppTemplateUrl, 'utf-8') | ||
: await fs.readFile(new URL('../../templates/app.html', import.meta.url), 'utf-8'); | ||
let mergedTemplateContents = ''; | ||
const pageRoot = htmlparser.parse(pageTemplateContents, { | ||
script: true, | ||
style: true, | ||
noscript: true, | ||
pre: true | ||
}); | ||
const appRoot = htmlparser.parse(appTemplateContents, { | ||
script: true, | ||
style: true | ||
}); | ||
if (!pageRoot.valid || !appRoot.valid) { | ||
console.debug('ERROR: Invalid HTML detected'); | ||
const invalidContents = !pageRoot.valid | ||
? pageTemplateContents | ||
: appTemplateContents; | ||
if (enableHud) { | ||
appTemplateContents = appTemplateContents.replace('<body>', ` | ||
<body> | ||
<div style="position: absolute; width: auto; border: dotted 3px red; background-color: white; opacity: 0.75; padding: 1% 1% 0"> | ||
<p>Malformed HTML detected, please check your closing tags or an <a href="https://www.google.com/search?q=html+formatter" target="_blank" rel="noreferrer">HTML formatter</a>.</p> | ||
<details> | ||
<pre> | ||
${invalidContents.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')} | ||
</pre> | ||
</details> | ||
</div> | ||
`); | ||
} | ||
mergedTemplateContents = appTemplateContents.replace(/<page-outlet><\/page-outlet>/, ''); | ||
} else { | ||
const appTitle = appRoot ? appRoot.querySelector('head title') : null; | ||
const appBody = appRoot.querySelector('body') ? appRoot.querySelector('body').innerHTML : ''; | ||
const pageBody = pageRoot.querySelector('body') ? pageRoot.querySelector('body').innerHTML : ''; | ||
const pageTitle = pageRoot.querySelector('head title'); | ||
const hasInterpolatedFrontmatter = pageTitle && pageTitle.rawText.indexOf('${globalThis.page.title}') >= 0 | ||
|| appTitle && appTitle.rawText.indexOf('${globalThis.page.title}') >= 0; | ||
const title = hasInterpolatedFrontmatter // favor frontmatter interpolation first | ||
? pageTitle && pageTitle.rawText | ||
? pageTitle.rawText | ||
: appTitle.rawText | ||
: frontmatterTitle // otherwise, work in order of specificity from page -> page template -> app template | ||
? frontmatterTitle | ||
: pageTitle && pageTitle.rawText | ||
? pageTitle.rawText | ||
: appTitle && appTitle.rawText | ||
? appTitle.rawText | ||
: 'My App'; | ||
const mergedHtml = pageRoot.querySelector('html').rawAttrs !== '' | ||
? `<html ${pageRoot.querySelector('html').rawAttrs}>` | ||
: appRoot.querySelector('html').rawAttrs !== '' | ||
? `<html ${appRoot.querySelector('html').rawAttrs}>` | ||
: '<html>'; | ||
const mergedMeta = [ | ||
...appRoot.querySelectorAll('head meta'), | ||
...pageRoot.querySelectorAll('head meta') | ||
].join('\n'); | ||
const mergedLinks = [ | ||
...appRoot.querySelectorAll('head link'), | ||
...pageRoot.querySelectorAll('head link') | ||
].join('\n'); | ||
const mergedStyles = [ | ||
...appRoot.querySelectorAll('head style'), | ||
...pageRoot.querySelectorAll('head style'), | ||
...customImports.filter(resource => path.extname(resource) === '.css') | ||
.map(resource => `<link rel="stylesheet" href="${resource}"></link>`) | ||
].join('\n'); | ||
const mergedScripts = [ | ||
...appRoot.querySelectorAll('head script'), | ||
...pageRoot.querySelectorAll('head script'), | ||
...customImports.filter(resource => path.extname(resource) === '.js') | ||
.map(resource => `<script src="${resource}" type="module"></script>`) | ||
].join('\n'); | ||
mergedTemplateContents = `<!DOCTYPE html> | ||
${mergedHtml} | ||
<head> | ||
<title>${title}</title> | ||
${mergedMeta} | ||
${mergedLinks} | ||
${mergedStyles} | ||
${mergedScripts} | ||
</head> | ||
<body> | ||
${appBody.replace(/<page-outlet><\/page-outlet>/, pageBody)} | ||
</body> | ||
</html> | ||
`; | ||
} | ||
return mergedTemplateContents; | ||
}; | ||
const getUserScripts = async (contents, context) => { | ||
// https://lit.dev/docs/tools/requirements/#polyfills | ||
if (process.env.__GWD_COMMAND__ === 'build') { // eslint-disable-line no-underscore-dangle | ||
const userPackageJson = await getPackageJson(context); | ||
const dependencies = userPackageJson?.dependencies || {}; | ||
const litPolyfill = dependencies && dependencies.lit | ||
? '<script src="/node_modules/lit/polyfill-support.js"></script>\n' | ||
: ''; | ||
contents = contents.replace('<head>', ` | ||
<head> | ||
${litPolyfill} | ||
`); | ||
} | ||
return contents; | ||
}; | ||
class StandardHtmlResource extends ResourceInterface { | ||
@@ -225,4 +39,4 @@ constructor(compilation, options) { | ||
async serve(url) { | ||
const { config } = this.compilation; | ||
const { pagesDir, userTemplatesDir, userWorkspace } = this.compilation.context; | ||
const { config, context } = this.compilation; | ||
const { pagesDir, userWorkspace } = context; | ||
const { interpolateFrontmatter } = config; | ||
@@ -352,7 +166,7 @@ const { pathname } = url; | ||
} else { | ||
body = ssrTemplate ? ssrTemplate : await getPageTemplate(filePath, this.compilation.context, template, contextPlugins); | ||
body = ssrTemplate ? ssrTemplate : await getPageTemplate(filePath, context, template, contextPlugins); | ||
} | ||
body = await getAppTemplate(body, userTemplatesDir, customImports, contextPlugins, config.devServer.hud, title); | ||
body = await getUserScripts(body, this.compilation.context); | ||
body = await getAppTemplate(body, context, customImports, contextPlugins, config.devServer.hud, title); | ||
body = await getUserScripts(body, context); | ||
@@ -359,0 +173,0 @@ if (processedMarkdown) { |
@@ -23,3 +23,3 @@ import fs from 'fs/promises'; | ||
.filter(plugin => plugin.type === 'resource') | ||
.map((plugin) => plugin.provider(this.compilation).extensions.flat()) | ||
.map((plugin) => plugin.provider(this.compilation).extensions || [].flat()) | ||
.flat(); | ||
@@ -29,3 +29,3 @@ const customPluginsExtensions = this.compilation.config.plugins | ||
.map((plugin) => { | ||
return plugin.provider(this.compilation).extensions.flat(); | ||
return plugin.provider(this.compilation).extensions || [].flat(); | ||
}).flat(); | ||
@@ -32,0 +32,0 @@ |
164138
53
3850
23
40
+ Added@rollup/plugin-commonjs@21.1.0(transitive)
+ Added@rollup/pluginutils@5.1.4(transitive)
+ Added@types/estree@1.0.6(transitive)
+ Added@web/rollup-plugin-import-meta-assets@1.0.8(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedcommondir@1.0.1(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addedestree-walker@2.0.2(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedglob@7.2.3(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedis-reference@1.2.1(transitive)
+ Addedmagic-string@0.30.17(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedpicomatch@4.0.2(transitive)
+ Addedwc-compiler@0.8.0(transitive)
+ Addedwrappy@1.0.2(transitive)
- Removedwc-compiler@0.6.2(transitive)
Updatedwc-compiler@~0.8.0