New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@greenwood/cli

Package Overview
Dependencies
Maintainers
1
Versions
160
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@greenwood/cli - npm Package Compare versions

Comparing version 0.28.0-alpha.4 to 0.28.0-alpha.5

src/lib/templating-utils.js

8

package.json
{
"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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')}
</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 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc