diff --git a/greenwood.config.js b/greenwood.config.js index 332980001..525eeee88 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -6,10 +6,9 @@ import { greenwoodPluginPolyfills } from '@greenwood/plugin-polyfills'; import { greenwoodPluginPostCss } from '@greenwood/plugin-postcss'; import { greenwoodPluginRendererPuppeteer } from '@greenwood/plugin-renderer-puppeteer'; import rollupPluginAnalyzer from 'rollup-plugin-analyzer'; -import { fileURLToPath, URL } from 'url'; export default { - workspace: fileURLToPath(new URL('./www', import.meta.url)), + workspace: new URL('./www/', import.meta.url), prerender: true, optimization: 'inline', staticRouter: true, @@ -20,6 +19,8 @@ export default { greenwoodPluginPostCss(), greenwoodPluginImportJson(), greenwoodPluginImportCss(), + greenwoodPluginIncludeHTML(), + greenwoodPluginRendererPuppeteer(), { type: 'rollup', name: 'rollup-plugin-analyzer', @@ -33,9 +34,7 @@ export default { }) ]; } - }, - greenwoodPluginIncludeHTML(), - greenwoodPluginRendererPuppeteer() + } ], markdown: { plugins: [ diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index e5ce43348..b8b9e142a 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -1,6 +1,7 @@ import { bundleCompilation } from '../lifecycles/bundle.js'; +import { checkResourceExists } from '../lib/resource-utils.js'; import { copyAssets } from '../lifecycles/copy.js'; -import fs from 'fs'; +import fs from 'fs/promises'; import { preRenderCompilationWorker, preRenderCompilationCustom, staticRenderCompilation } from '../lifecycles/prerender.js'; import { ServerInterface } from '../lib/server-interface.js'; @@ -11,12 +12,14 @@ const runProductionBuild = async (compilation) => { try { const { prerender } = compilation.config; const outputDir = compilation.context.outputDir; - const prerenderPlugin = (compilation.config.plugins.filter(plugin => plugin.type === 'renderer') || []).length === 1 - ? compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation) + const prerenderPlugin = compilation.config.plugins.find(plugin => plugin.type === 'renderer') + ? compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider(compilation) : {}; - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir); + if (!await checkResourceExists(outputDir)) { + await fs.mkdir(outputDir, { + recursive: true + }); } if (prerender || prerenderPlugin.prerender) { diff --git a/packages/cli/src/commands/eject.js b/packages/cli/src/commands/eject.js index e93792ff7..886b74e2a 100644 --- a/packages/cli/src/commands/eject.js +++ b/packages/cli/src/commands/eject.js @@ -1,21 +1,19 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath, URL } from 'url'; +import fs from 'fs/promises'; const ejectConfiguration = async (compilation) => { return new Promise(async (resolve, reject) => { try { - const configFilePath = fileURLToPath(new URL('../config', import.meta.url)); - const configFiles = fs.readdirSync(configFilePath); + const configFileDirUrl = new URL('../config/', import.meta.url); + const configFiles = await fs.readdir(configFileDirUrl); - configFiles.forEach((configFile) => { - const from = path.join(configFilePath, configFile); - const to = `${compilation.context.projectDirectory}/${configFile}`; + for (const file of configFiles) { + const from = new URL(`./${file}`, configFileDirUrl); + const to = new URL(`./${file}`, compilation.context.projectDirectory); - fs.copyFileSync(from, to); + await fs.copyFile(from, to); - console.log(`Ejected ${configFile} successfully.`); - }); + console.log(`Ejected ${file} successfully.`); + } console.debug('all configuration files ejected.'); diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js index 0fec32a25..2e6e34f97 100644 --- a/packages/cli/src/config/rollup.config.js +++ b/packages/cli/src/config/rollup.config.js @@ -1,5 +1,5 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'fs/promises'; +import { checkResourceExists, normalizePathnameForWindows, resolveForRelativeUrl } from '../lib/resource-utils.js'; function greenwoodResourceLoader (compilation) { const resourcePlugins = compilation.config.plugins.filter((plugin) => { @@ -10,43 +10,42 @@ function greenwoodResourceLoader (compilation) { return { name: 'greenwood-resource-loader', - resolveId(id) { - const { userWorkspace } = compilation.context; + async resolveId(id) { + const normalizedId = id.replace(/\?type=(.*)/, ''); + const { projectDirectory, userWorkspace } = compilation.context; - if ((id.indexOf('./') === 0 || id.indexOf('/') === 0) && fs.existsSync(path.join(userWorkspace, id))) { - return path.join(userWorkspace, id.replace(/\?type=(.*)/, '')); - } + if (id.startsWith('.') || id.startsWith('/')) { + const prefix = id.startsWith('/') ? '.' : ''; + const contextUrl = id.indexOf('/node_modules/') >= 0 ? projectDirectory : userWorkspace; + const userWorkspaceUrl = await resolveForRelativeUrl(new URL(`${prefix}${normalizedId}`, contextUrl), contextUrl); - return null; + if (await checkResourceExists(userWorkspaceUrl)) { + return normalizePathnameForWindows(userWorkspaceUrl); + } + } }, async load(id) { - const importAsIdAsUrl = id.replace(/\?type=(.*)/, ''); - const extension = path.extname(importAsIdAsUrl); + const pathname = id.indexOf('?') >= 0 ? id.slice(0, id.indexOf('?')) : id; + const extension = pathname.split('.').pop(); - if (extension !== '.js') { - const originalUrl = `${id}?type=${extension.replace('.', '')}`; - let contents; + if (extension !== '' && extension !== 'js') { + const url = new URL(`file://${pathname}?type=${extension}`); + const request = new Request(url.href); + let response = new Response(''); for (const plugin of resourcePlugins) { - const headers = { - request: { - originalUrl - }, - response: { - 'content-type': plugin.contentType - } - }; - - contents = await plugin.shouldServe(importAsIdAsUrl) - ? (await plugin.serve(importAsIdAsUrl)).body - : contents; + if (plugin.shouldServe && await plugin.shouldServe(url, request)) { + response = await plugin.serve(url, request); + } + } - if (await plugin.shouldIntercept(importAsIdAsUrl, contents, headers)) { - contents = (await plugin.intercept(importAsIdAsUrl, contents, headers)).body; + for (const plugin of resourcePlugins) { + if (plugin.shouldIntercept && await plugin.shouldIntercept(url, request, response.clone())) { + response = await plugin.intercept(url, request, response.clone()); } } - return contents; + return await response.text(); } } }; @@ -55,15 +54,14 @@ function greenwoodResourceLoader (compilation) { function greenwoodSyncPageResourceBundlesPlugin(compilation) { return { name: 'greenwood-sync-page-resource-bundles-plugin', - writeBundle(outputOptions, bundles) { + async writeBundle(outputOptions, bundles) { const { outputDir } = compilation.context; for (const resource of compilation.resources.values()) { - const resourceKey = resource.sourcePathURL.pathname; + const resourceKey = normalizePathnameForWindows(resource.sourcePathURL); for (const bundle in bundles) { let facadeModuleId = (bundles[bundle].facadeModuleId || '').replace(/\\/g, '/'); - /* * this is an odd issue related to symlinking in our Greenwood monorepo when building the website * and managing packages that we create as "virtual" modules, like for the mpa router @@ -82,25 +80,27 @@ function greenwoodSyncPageResourceBundlesPlugin(compilation) { * pathToMatch (before): /node_modules/@greenwood/cli/src/lib/router.js * pathToMatch (after): /cli/src/lib/router.js */ - if (facadeModuleId && resourceKey.indexOf('/node_modules/@greenwood/cli') > 0 && facadeModuleId.indexOf('/packages/cli') > 0 && fs.existsSync(facadeModuleId)) { - facadeModuleId = facadeModuleId.replace('/packages/cli', '/node_modules/@greenwood/cli'); + if (resourceKey?.indexOf('/node_modules/@greenwood/cli') > 0 && facadeModuleId?.indexOf('/packages/cli') > 0) { + if (await checkResourceExists(new URL(`file://${facadeModuleId}`))) { + facadeModuleId = facadeModuleId.replace('/packages/cli', '/node_modules/@greenwood/cli'); + } } if (resourceKey === facadeModuleId) { const { fileName } = bundles[bundle]; const { rawAttributes, contents } = resource; const noop = rawAttributes && rawAttributes.indexOf('data-gwd-opt="none"') >= 0 || compilation.config.optimization === 'none'; - const outputPath = path.join(outputDir, fileName); + const outputPath = new URL(`./${fileName}`, outputDir); - compilation.resources.set(resourceKey, { - ...compilation.resources.get(resourceKey), + compilation.resources.set(resource.sourcePathURL.pathname, { + ...compilation.resources.get(resource.sourcePathURL.pathname), optimizedFileName: fileName, - optimizedFileContents: fs.readFileSync(outputPath, 'utf-8'), + optimizedFileContents: await fs.readFile(outputPath, 'utf-8'), contents: contents.replace(/\.\//g, '/') }); if (noop) { - fs.writeFileSync(outputPath, contents); + await fs.writeFile(outputPath, contents); } } } @@ -113,7 +113,7 @@ const getRollupConfig = async (compilation) => { const { outputDir } = compilation.context; const input = [...compilation.resources.values()] .filter(resource => resource.type === 'script') - .map(resource => resource.sourcePathURL.pathname); + .map(resource => normalizePathnameForWindows(resource.sourcePathURL)); const customRollupPlugins = compilation.config.plugins.filter(plugin => { return plugin.type === 'rollup'; }).map(plugin => { @@ -124,7 +124,7 @@ const getRollupConfig = async (compilation) => { preserveEntrySignatures: 'strict', // https://github.com/ProjectEvergreen/greenwood/pull/990 input, output: { - dir: outputDir, + dir: normalizePathnameForWindows(outputDir), entryFileNames: '[name].[hash].js', chunkFileNames: '[name].[hash].js', sourcemap: true diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index f398ed2e7..566b68ac2 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -6,11 +6,11 @@ process.setMaxListeners(0); import { generateCompilation } from './lifecycles/compile.js'; -import fs from 'fs'; +import fs from 'fs/promises'; import program from 'commander'; import { URL } from 'url'; -const greenwoodPackageJson = JSON.parse(await fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf-8')); +const greenwoodPackageJson = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')); let cmdOption = {}; let command = ''; diff --git a/packages/cli/src/lib/node-modules-utils.js b/packages/cli/src/lib/node-modules-utils.js index 393d03c9f..2a45af8ad 100644 --- a/packages/cli/src/lib/node-modules-utils.js +++ b/packages/cli/src/lib/node-modules-utils.js @@ -1,6 +1,8 @@ +// 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 fs from 'fs'; -import path from 'path'; +import { checkResourceExists } from '../lib/resource-utils.js'; +import fs from 'fs/promises'; // defer to NodeJS to find where on disk a package is located using import.meta.resolve // and return the root absolute location @@ -35,14 +37,14 @@ async function getNodeModulesLocationForPackage(packageName) { const nodeModulesPackageRoot = `${locations[location]}/${packageName}`; const packageJsonLocation = `${nodeModulesPackageRoot}/package.json`; - if (fs.existsSync(packageJsonLocation)) { + if (await checkResourceExists(new URL(`file://${packageJsonLocation}`))) { nodeModulesUrl = nodeModulesPackageRoot; } } if (!nodeModulesUrl) { console.debug(`Unable to look up ${packageName} using NodeJS require.resolve. Falling back to process.cwd()`); - nodeModulesUrl = path.join(process.cwd(), 'node_modules', packageName); // force / for consistency and path matching); + nodeModulesUrl = new URL(`./node_modules/${packageName}`, `file://${process.cwd()}`).pathname; } } @@ -62,7 +64,21 @@ function getPackageNameFromUrl(url) { return packageName; } +async function getPackageJson({ userWorkspace, projectDirectory }) { + const monorepoPackageJsonUrl = new URL('./package.json', userWorkspace); + const topLevelPackageJsonUrl = new URL('./package.json', projectDirectory); + const hasMonorepoPackageJson = await checkResourceExists(monorepoPackageJsonUrl); + const hasTopLevelPackageJson = await checkResourceExists(topLevelPackageJsonUrl); + + return hasMonorepoPackageJson // handle monorepos first + ? JSON.parse(await fs.readFile(monorepoPackageJsonUrl, 'utf-8')) + : hasTopLevelPackageJson + ? JSON.parse(await fs.readFile(topLevelPackageJsonUrl, 'utf-8')) + : {}; +} + export { getNodeModulesLocationForPackage, + getPackageJson, getPackageNameFromUrl }; \ No newline at end of file diff --git a/packages/cli/src/lib/resource-interface.js b/packages/cli/src/lib/resource-interface.js index adb07900d..bee66b637 100644 --- a/packages/cli/src/lib/resource-interface.js +++ b/packages/cli/src/lib/resource-interface.js @@ -1,96 +1,8 @@ -import fs from 'fs'; -import path from 'path'; - class ResourceInterface { constructor(compilation, options = {}) { this.compilation = compilation; this.options = options; this.extensions = []; - this.contentType = ''; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types - } - - // get rid of things like query string parameters - // that will break when trying to use with fs - getBareUrlPath(url) { - return url.replace(/\?(.*)/, ''); - } - - // turn relative paths into relatively absolute based on a known root directory - // e.g. "../styles/theme.css" -> `${userWorkspace}/styles/theme.css` - resolveRelativeUrl(root, url) { - if (fs.existsSync(path.join(root, url))) { - return url; - } - - let reducedUrl; - - url.split('/') - .filter((segment) => segment !== '') - .reduce((acc, segment) => { - const reducedPath = url.replace(`${acc}/${segment}`, ''); - - if (path.extname(reducedPath) !== '' && fs.existsSync(path.join(root, reducedPath))) { - reducedUrl = reducedPath; - } - return `${acc}/${segment}`; - }, ''); - - return reducedUrl; - } - - // test if this plugin should change a relative URL from the browser to an absolute path on disk - // like for node_modules/ resolution. not commonly needed by most resource plugins - // return true | false - // eslint-disable-next-line no-unused-vars - async shouldResolve(url) { - return Promise.resolve(false); - } - - // return an absolute path - async resolve(url) { - return Promise.resolve(url); - } - - // test if this plugin should be used to process a given url / header combo the browser and retu - // ex: ` - return modelResource(context, 'script', src, null, optimizationAttr, rawAttrs); + return await modelResource(context, 'script', src, null, optimizationAttr, rawAttrs); } else if (script.rawText) { // - return modelResource(context, 'script', null, script.rawText, optimizationAttr, rawAttrs); + return await modelResource(context, 'script', null, script.rawText, optimizationAttr, rawAttrs); } - }); + })); - const styles = root.querySelectorAll('style') + const styles = await Promise.all(root.querySelectorAll('style') .filter(style => !(/\$/).test(style.rawText) && !(//).test(style.rawText)) // filter out Shady DOM - `).replace(``, ` - - `); - } - } + const { pathname } = url; + const pageResources = this.compilation.graph.find(page => page.outputPath === pathname || page.route === pathname).imports; + let body = await response.text(); + + for (const pageResource of pageResources) { + const keyedResource = this.compilation.resources.get(pageResource.sourcePathURL.pathname); + const { contents, src, type, optimizationAttr, optimizedFileContents, optimizedFileName, rawAttributes } = keyedResource; + + if (src) { + if (type === 'script') { + if (!optimizationAttr && optimization === 'default') { + const optimizedFilePath = `/${optimizedFileName}`; + + body = body.replace(src, optimizedFilePath); + body = body.replace('', ` + + + `); + } else if (optimizationAttr === 'inline' || optimization === 'inline') { + const isModule = rawAttributes.indexOf('type="module') >= 0 ? ' type="module"' : ''; + + body = body.replace(``, ` + + `); + } else if (optimizationAttr === 'static' || optimization === 'static') { + body = body.replace(``, ''); + } + } else if (type === 'link') { + if (!optimizationAttr && (optimization !== 'none' && optimization !== 'inline')) { + const optimizedFilePath = `/${optimizedFileName}`; + + body = body.replace(src, optimizedFilePath); + body = body.replace('', ` + + + `); + } else if (optimizationAttr === 'inline' || optimization === 'inline') { + // https://github.com/ProjectEvergreen/greenwood/issues/810 + // when pre-rendering, puppeteer normalizes everything to + // but if not using pre-rendering, then it could come out as + // not great, but best we can do for now until #742 + body = body.replace(``, ` + + `).replace(``, ` + + `); + } + } + } else { + if (type === 'script') { + if (optimizationAttr === 'static' || optimization === 'static') { + body = body.replace(``, ''); + } else if (optimizationAttr === 'none') { + body = body.replace(contents, contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')); } else { - if (type === 'script') { - if (optimizationAttr === 'static' || optimization === 'static') { - body = body.replace(``, ''); - } else if (optimizationAttr === 'none') { - body = body.replace(contents, contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')); - } else { - body = body.replace(contents, optimizedFileContents.replace(/\.\//g, '/').replace(/\$/g, '$$$')); - } - } else if (type === 'style') { - body = body.replace(contents, optimizedFileContents); - } + body = body.replace(contents, optimizedFileContents.replace(/\.\//g, '/').replace(/\$/g, '$$$')); } + } else if (type === 'style') { + body = body.replace(contents, optimizedFileContents); } + } + } - // TODO clean up lit-polyfill as part of https://github.com/ProjectEvergreen/greenwood/issues/728 - body = body.replace(/\n - - `); - - resolve({ body }); - } catch (e) { - reject(e); - } - }); - } + async intercept(url, request, response) { + let body = await response.text(); - async shouldOptimize(url, body, headers = { request: {} }) { - const contentType = headers.request['content-type']; + body = body.replace('', ` + \n + + `); - return Promise.resolve(this.compilation.config.staticRouter - && url !== '404.html' - && (path.extname(url) === '.html' || (contentType && contentType.indexOf('text/html') >= 0))); + return new Response(body); } - async optimize(url, body) { - return new Promise(async (resolve, reject) => { - try { - let currentTemplate; - const isStaticRoute = this.compilation.graph.filter(page => page.outputPath === url && url !== '/404/' && !page.isSSR).length === 1; - const { outputDir } = this.compilation.context; - const bodyContents = body.match(/(.*)<\/body>/s)[0].replace('', '').replace('', ''); - const outputBundlePath = path.join(`${outputDir}/_routes${url}`); - - const routeTags = this.compilation.graph - .filter(page => !page.isSSR) - .filter(page => page.route !== '/404/') - .map((page) => { - const template = page.filename && path.extname(page.filename) === '.html' - ? page.route - : page.template; - const key = page.route === '/' - ? '' - : page.route.slice(0, page.route.lastIndexOf('/')); - - if (url === page.outputPath) { - currentTemplate = template; - } - return ` - - `; - }); - - if (isStaticRoute) { - if (!fs.existsSync(path.dirname(outputBundlePath))) { - fs.mkdirSync(path.dirname(outputBundlePath), { - recursive: true - }); - } - - await fs.promises.writeFile(outputBundlePath, bodyContents); - } + async shouldOptimize(url, response) { + return this.compilation.config.staticRouter + && !url.pathname.startsWith('/404') + && response.headers.get('Content-Type').indexOf(this.contentType) >= 0; + } - body = body.replace('', ` - - - `.replace(/\n/g, '').replace(/ /g, '')) - .replace(/(.*)<\/body>/s, ` - \n - - - ${bodyContents.replace(/\$/g, '$$$')}\n - - - ${routeTags.join('\n')} - - `); - - resolve(body); - } catch (e) { - reject(e); + async optimize(url, response) { + let body = await response.text(); + 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>/s)[0].replace('', '').replace('', ''); + const outputPartialDirUrl = new URL(`./_routes${url.pathname}`, 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/') + .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('/')); + + if (pathname === page.route) { + currentTemplate = template; + } + return ` + + `; + }); + + if (isStaticRoute) { + if (!await checkResourceExists(outputPartialDirPathUrl)) { + await fs.mkdir(outputPartialDirPathUrl, { + recursive: true + }); } - }); + + await fs.writeFile(new URL('./index.html', outputPartialDirUrl), partial); + } + + body = body.replace('', ` + + + `.replace(/\n/g, '').replace(/ /g, '')) + .replace(/(.*)<\/body>/s, ` + \n + + + ${partial.replace(/\$/g, '$$$')}\n + + + ${routeTags.join('\n')} + + `); + + return new Response(body); } } diff --git a/packages/cli/src/plugins/resource/plugin-user-workspace.js b/packages/cli/src/plugins/resource/plugin-user-workspace.js index 2c308d725..cb67c6041 100644 --- a/packages/cli/src/plugins/resource/plugin-user-workspace.js +++ b/packages/cli/src/plugins/resource/plugin-user-workspace.js @@ -4,42 +4,29 @@ * This sets the default value for requests in Greenwood. * */ -import fs from 'fs'; -import path from 'path'; +import { resolveForRelativeUrl } from '../../lib/resource-utils.js'; import { ResourceInterface } from '../../lib/resource-interface.js'; class UserWorkspaceResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['*']; } - async shouldResolve(url = '/') { + async shouldResolve(url) { const { userWorkspace } = this.compilation.context; - const bareUrl = this.getBareUrlPath(url); - const isAbsoluteWorkspaceFile = fs.existsSync(path.join(userWorkspace, bareUrl)); - const workspaceUrl = isAbsoluteWorkspaceFile - ? isAbsoluteWorkspaceFile || bareUrl === '/' - : url.indexOf('node_modules') < 0 && this.resolveRelativeUrl(userWorkspace, bareUrl); + const extension = url.pathname.split('.').pop(); + const hasExtension = extension !== '' && !extension.startsWith('/'); - return Promise.resolve(workspaceUrl); + return hasExtension + && !url.pathname.startsWith('/node_modules') + && await resolveForRelativeUrl(url, userWorkspace); } - async resolve(url = '/') { + async resolve(url) { const { userWorkspace } = this.compilation.context; + const workspaceUrl = await resolveForRelativeUrl(url, userWorkspace); - return new Promise(async (resolve, reject) => { - try { - const bareUrl = this.getBareUrlPath(url); - const workspaceUrl = fs.existsSync(path.join(userWorkspace, bareUrl)) - ? path.join(userWorkspace, bareUrl) - : path.join(userWorkspace, this.resolveRelativeUrl(userWorkspace, bareUrl)); - - resolve(workspaceUrl); - } catch (e) { - reject(e); - } - }); + return new Request(workspaceUrl); } } diff --git a/packages/cli/src/plugins/server/plugin-livereload.js b/packages/cli/src/plugins/server/plugin-livereload.js index 82deca0b1..8b974179c 100644 --- a/packages/cli/src/plugins/server/plugin-livereload.js +++ b/packages/cli/src/plugins/server/plugin-livereload.js @@ -1,8 +1,7 @@ -import fs from 'fs'; +import fs from 'fs/promises'; import livereload from 'livereload'; import { ResourceInterface } from '../../lib/resource-interface.js'; import { ServerInterface } from '../../lib/server-interface.js'; -import { fileURLToPath, pathToFileURL } from 'url'; class LiveReloadServer extends ServerInterface { constructor(compilation, options = {}) { @@ -11,23 +10,17 @@ class LiveReloadServer extends ServerInterface { async start() { const { userWorkspace } = this.compilation.context; - const standardPluginsPath = fileURLToPath(new URL('../resource', import.meta.url)); - const standardPluginsNames = fs.readdirSync(standardPluginsPath) + const standardPluginsDirectoryPath = new URL('../resource/', import.meta.url); + const standardPluginsNames = (await fs.readdir(standardPluginsDirectoryPath)) .filter(filename => filename.indexOf('plugin-standard') === 0); const standardPluginsExtensions = (await Promise.all(standardPluginsNames.map(async (filename) => { - const pluginImport = await import(pathToFileURL(`${standardPluginsPath}/${filename}`)); + const pluginImport = await import(new URL(`./${filename}`, standardPluginsDirectoryPath)); const plugin = pluginImport[Object.keys(pluginImport)[0]]; return plugin; }))) - .map((plugin) => { - // assume that if it is an array, the second item is a rollup plugin - const instance = plugin.length - ? plugin[0].provider(this.compilation) - : plugin.provider(this.compilation); - - return instance.extensions.flat(); - }) + .filter(plugin => plugin.type === 'resource') + .map((plugin) => plugin.provider(this.compilation).extensions.flat()) .flat(); const customPluginsExtensions = this.compilation.config.plugins .filter((plugin) => plugin.type === 'resource') @@ -49,7 +42,7 @@ class LiveReloadServer extends ServerInterface { applyCSSLive: false // https://github.com/napcs/node-livereload/issues/33#issuecomment-693707006 }); - liveReloadServer.watch(userWorkspace, () => { + liveReloadServer.watch(userWorkspace.pathname, () => { console.info(`Now watching directory "${userWorkspace}" for changes.`); return Promise.resolve(true); }); @@ -58,25 +51,21 @@ class LiveReloadServer extends ServerInterface { class LiveReloadResource extends ResourceInterface { - async shouldIntercept(url, body, headers) { - const { accept } = headers.request; + async shouldIntercept(url, request, response) { + const contentType = response.headers.get('Content-Type'); - return Promise.resolve(accept && accept.indexOf('text/html') >= 0 && process.env.__GWD_COMMAND__ === 'develop'); // eslint-disable-line no-underscore-dangle + return contentType.indexOf('text/html') >= 0 && process.env.__GWD_COMMAND__ === 'develop'; // eslint-disable-line no-underscore-dangle } - async intercept(url, body) { - return new Promise((resolve, reject) => { - try { - const contents = body.replace('', ` - - - `); + async intercept(url, request, response) { + let body = await response.text(); + + body = body.replace('', ` + + + `); - resolve({ body: contents }); - } catch (e) { - reject(e); - } - }); + return new Response(body); } } diff --git a/packages/cli/test/cases/build.config.error-workspace-absolute/greenwood.config.js b/packages/cli/test/cases/build.config.error-workspace-absolute/greenwood.config.js index 3c0eb5fe5..fd2a060f4 100644 --- a/packages/cli/test/cases/build.config.error-workspace-absolute/greenwood.config.js +++ b/packages/cli/test/cases/build.config.error-workspace-absolute/greenwood.config.js @@ -1,5 +1,3 @@ -import { fileURLToPath, URL } from 'url'; - export default { - workspace: fileURLToPath(new URL('./noop', import.meta.url)) + workspace: new URL('./noop', import.meta.url) }; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.error-workspace/build.config.error-workspace.spec.js b/packages/cli/test/cases/build.config.error-workspace/build.config.error-workspace.spec.js index 75c5a2e3c..82cd986a7 100644 --- a/packages/cli/test/cases/build.config.error-workspace/build.config.error-workspace.spec.js +++ b/packages/cli/test/cases/build.config.error-workspace/build.config.error-workspace.spec.js @@ -36,12 +36,12 @@ describe('Build Greenwood With: ', function() { }); describe('Custom Configuration with a bad value for Workspace', function() { - it('should throw an error that workspace path must be a string', async function() { + it('should throw an error that workspace path must be a URL', async function() { try { await runner.setup(outputPath); await runner.runCommand(cliPath, 'build'); } catch (err) { - expect(err).to.contain('greenwood.config.js workspace path must be a string'); + expect(err).to.contain('Error: greenwood.config.js workspace must be an instance of URL'); } }); }); diff --git a/packages/cli/test/cases/build.config.workspace-custom/greenwood.config.js b/packages/cli/test/cases/build.config.workspace-custom/greenwood.config.js index cef9426a0..5adb52a1e 100644 --- a/packages/cli/test/cases/build.config.workspace-custom/greenwood.config.js +++ b/packages/cli/test/cases/build.config.workspace-custom/greenwood.config.js @@ -1,5 +1,3 @@ -import { fileURLToPath, URL } from 'url'; - export default { - workspace: fileURLToPath(new URL('./www', import.meta.url)) + workspace: new URL('./www/', import.meta.url) }; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js b/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js index 155bf389d..58ac4a43e 100644 --- a/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js +++ b/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js @@ -86,7 +86,7 @@ describe('Build Greenwood With: ', function() { before(async function() { const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, 'public/graph.json'), 'utf-8')); - artistsPageGraphData = graph.filter(page => page.route === '/artists/')[0]; + artistsPageGraphData = graph.find(page => page.route === '/artists/'); return new Promise((resolve, reject) => { request.get(`${hostname}/artists/`, (err, res, body) => { diff --git a/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js b/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js index 5bac46467..56310dcbf 100644 --- a/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js +++ b/packages/cli/test/cases/build.plugins.context/theme-pack-context-plugin.js @@ -1,23 +1,20 @@ -import fs from 'fs'; +import fs from 'fs/promises'; import os from 'os'; -import path from 'path'; import { spawnSync } from 'child_process'; -import { fileURLToPath, URL } from 'url'; -const packageJson = JSON.parse(await fs.promises.readFile(new URL('./package.json', import.meta.url), 'utf-8')); +const packageJson = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf-8')); const myThemePackPlugin = () => [{ type: 'context', name: 'my-theme-pack:context', provider: () => { const { name } = packageJson; - const baseDistDir = `node_modules/${name}/dist`; const command = os.platform() === 'win32' ? 'npm.cmd' : 'npm'; const ls = spawnSync(command, ['ls', name]); const isInstalled = ls.stdout.toString().indexOf('(empty)') < 0; const templateLocation = isInstalled - ? fileURLToPath(new URL(`${baseDistDir}/layouts`, import.meta.url)) - : path.join(process.cwd(), 'fixtures/layouts'); + ? new URL(`./node_modules/${name}/dist/layouts/`, import.meta.url) + : new URL('./fixtures/layouts/', import.meta.url); return { templates: [ diff --git a/packages/cli/test/cases/build.plugins.copy/greenwood.config.js b/packages/cli/test/cases/build.plugins.copy/greenwood.config.js index 26bc24c67..ac08ffe25 100644 --- a/packages/cli/test/cases/build.plugins.copy/greenwood.config.js +++ b/packages/cli/test/cases/build.plugins.copy/greenwood.config.js @@ -1,19 +1,14 @@ -import path from 'path'; - export default { plugins: [{ type: 'copy', name: 'plugin-copy-prismjs', provider: (compilation) => { const { projectDirectory, outputDir } = compilation.context; - const prismThemeDir = 'node_modules/prismjs/themes'; - const from = path.join(projectDirectory, prismThemeDir); - const to = path.join(outputDir, prismThemeDir); + const prismThemeDir = '/node_modules/prismjs/themes/'; + const from = new URL(`.${prismThemeDir}`, projectDirectory); + const to = new URL(`.${prismThemeDir}`, outputDir); - return [{ - from, - to - }]; + return [{ from, to }]; } }] }; \ No newline at end of file diff --git a/packages/cli/test/cases/build.plugins.resource/greenwood.config.js b/packages/cli/test/cases/build.plugins.resource/greenwood.config.js index 18265ec27..34dd63d20 100644 --- a/packages/cli/test/cases/build.plugins.resource/greenwood.config.js +++ b/packages/cli/test/cases/build.plugins.resource/greenwood.config.js @@ -1,28 +1,27 @@ -import fs from 'fs'; +import fs from 'fs/promises'; import { ResourceInterface } from '../../../src/lib/resource-interface.js'; class FooResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['.foo']; + this.extensions = ['foo']; this.contentType = 'text/javascript'; } + async shouldServe(url) { + return url.pathname.split('.').pop() === this.extensions[0]; + } + async serve(url) { - return new Promise(async (resolve, reject) => { - try { - let body = await fs.promises.readFile(url, 'utf-8'); - - body = body.replace(/interface (.*){(.*)}/s, ''); + let body = await fs.readFile(url, 'utf-8'); + + body = body.replace(/interface (.*){(.*)}/s, ''); - resolve({ - body, - contentType: this.contentType - }); - } catch (e) { - reject(e); - } + return new Response(body, { + headers: new Headers({ + 'Content-Type': this.contentType + }) }); } } diff --git a/packages/cli/test/cases/develop.default/develop.default.spec.js b/packages/cli/test/cases/develop.default/develop.default.spec.js index 5c1666f0b..b6bba33dc 100644 --- a/packages/cli/test/cases/develop.default/develop.default.spec.js +++ b/packages/cli/test/cases/develop.default/develop.default.spec.js @@ -47,6 +47,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; +// TODO good first issue, to abstract along with copy plugin async function rreaddir (dir, allFiles = []) { const files = (await fs.promises.readdir(dir)).map(f => path.join(dir, f)); @@ -640,7 +641,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -674,7 +675,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers['content-type']).to.contain('text/css'); done(); }); @@ -710,7 +711,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(`image/${ext}`); + expect(response.headers['content-type']).to.contain(`image/${ext}`); done(); }); @@ -744,7 +745,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('image/x-icon'); + expect(response.headers['content-type']).to.contain('image/x-icon'); done(); }); @@ -778,7 +779,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('image/webp'); + expect(response.headers['content-type']).to.contain('image/webp'); done(); }); @@ -812,7 +813,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('image/avif'); + expect(response.headers['content-type']).to.contain('image/avif'); done(); }); @@ -847,7 +848,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(`image/${ext}+xml`); + expect(response.headers['content-type']).to.contain(`image/${ext}+xml`); done(); }); @@ -882,7 +883,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(ext); + expect(response.headers['content-type']).to.contain(ext); done(); }); @@ -984,7 +985,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -1020,7 +1021,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers['content-type']).to.contain('text/css'); done(); }); @@ -1032,7 +1033,7 @@ describe('Develop Greenwood With: ', function() { // if things work correctly, this workspace file should never resolve to the equivalent node_modules file // https://github.com/ProjectEvergreen/greenwood/pull/687 - describe('Develop command specific workspace resolution when matching node_modules', function() { + describe('Develop command specific workspace resolution when local file matches a file also in node_modules', function() { let response = {}; before(async function() { @@ -1056,7 +1057,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -1091,7 +1092,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -1127,7 +1128,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -1167,7 +1168,12 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct response body', function(done) { - expect(response.body).to.contain('Not Found'); + expect(response.body).to.contain(''); + done(); + }); + + it('should return the correct status message body', function(done) { + expect(response.statusMessage).to.contain('Not Found'); done(); }); }); @@ -1214,6 +1220,7 @@ describe('Develop Greenwood With: ', function() { before(async function() { response = await fetch(`${hostname}:${port}/api/greeting?name=${name}`); + data = await response.json(); }); diff --git a/packages/cli/test/cases/develop.default/src/api/greeting.js b/packages/cli/test/cases/develop.default/src/api/greeting.js index f046e6243..09666902e 100644 --- a/packages/cli/test/cases/develop.default/src/api/greeting.js +++ b/packages/cli/test/cases/develop.default/src/api/greeting.js @@ -4,8 +4,8 @@ export async function handler(request) { const body = { message: `Hello ${name}!!!` }; return new Response(JSON.stringify(body), { - headers: { + headers: new Headers({ 'Content-Type': 'application/json' - } + }) }); } \ No newline at end of file diff --git a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js index 2e7f330b9..5a2fa593b 100644 --- a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js +++ b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js @@ -141,7 +141,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers['content-type']).to.contain('text/css'); done(); }); @@ -179,7 +179,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); diff --git a/packages/cli/test/cases/develop.plugins.context/greenwood.config.js b/packages/cli/test/cases/develop.plugins.context/greenwood.config.js index b3b0cec5b..74c311257 100644 --- a/packages/cli/test/cases/develop.plugins.context/greenwood.config.js +++ b/packages/cli/test/cases/develop.plugins.context/greenwood.config.js @@ -1,24 +1,24 @@ // shared from another test -import fs from 'fs'; -import path from 'path'; +import fs from 'fs/promises'; import { myThemePackPlugin } from '../build.plugins.context/theme-pack-context-plugin.js'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; -import { URL } from 'url'; -const packageName = JSON.parse(await fs.promises.readFile(new URL('./package.json', import.meta.url), 'utf-8')).name; +const packageName = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf-8')).name; class MyThemePackDevelopmentResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['*']; } async shouldResolve(url) { - return Promise.resolve(url.indexOf(`node_modules${path.sep}${packageName}`) >= 0); + return url.pathname.indexOf(`node_modules/${packageName}`) >= 0; } async resolve(url) { - return Promise.resolve(path.normalize(url).replace(`node_modules${path.sep}${packageName}${path.sep}dist`, 'fixtures')); + const themePackPathname = url.pathname.replace(`/node_modules/${packageName}/dist`, '/fixtures'); + const themePackUrl = new URL(`.${themePackPathname}`, import.meta.url); + + return new Request(themePackUrl); } } diff --git a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js index 1e4fde3fd..da11977ed 100644 --- a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js +++ b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js @@ -83,7 +83,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('application/json; charset=utf-8'); + expect(response.headers['content-type']).to.contain('application/json'); done(); }); diff --git a/packages/cli/test/cases/serve.default.api/src/api/greeting.js b/packages/cli/test/cases/serve.default.api/src/api/greeting.js index f046e6243..09666902e 100644 --- a/packages/cli/test/cases/serve.default.api/src/api/greeting.js +++ b/packages/cli/test/cases/serve.default.api/src/api/greeting.js @@ -4,8 +4,8 @@ export async function handler(request) { const body = { message: `Hello ${name}!!!` }; return new Response(JSON.stringify(body), { - headers: { + headers: new Headers({ 'Content-Type': 'application/json' - } + }) }); } \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default/serve.default.spec.js b/packages/cli/test/cases/serve.default/serve.default.spec.js index bd4a2bcf9..f66a7b872 100644 --- a/packages/cli/test/cases/serve.default/serve.default.spec.js +++ b/packages/cli/test/cases/serve.default/serve.default.spec.js @@ -226,7 +226,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal(ext); + expect(response.headers['content-type']).to.equal(`font/${ext}`); done(); }); diff --git a/packages/cli/test/cases/theme-pack/greenwood.config.js b/packages/cli/test/cases/theme-pack/greenwood.config.js index ae0d3417d..1e8502c98 100644 --- a/packages/cli/test/cases/theme-pack/greenwood.config.js +++ b/packages/cli/test/cases/theme-pack/greenwood.config.js @@ -1,24 +1,23 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'fs/promises'; import { myThemePack } from './my-theme-pack.js'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; -import { URL } from 'url'; -const packageName = JSON.parse(await fs.promises.readFile(new URL('./package.json', import.meta.url), 'utf-8')).name; +const packageName = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf-8')).name; class MyThemePackDevelopmentResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['*']; } async shouldResolve(url) { // eslint-disable-next-line no-underscore-dangle - return Promise.resolve(process.env.__GWD_COMMAND__ === 'develop' && url.indexOf(`node_modules${path.sep}${packageName}/`) >= 0); + return process.env.__GWD_COMMAND__ === 'develop' && url.pathname.startsWith(`/node_modules/${packageName}/`); } async resolve(url) { - return Promise.resolve(path.normalize(this.getBareUrlPath(url)).replace(`node_modules${path.sep}${packageName}${path.sep}dist`, 'src')); + const themePackUrl = url.pathname.replace(`/node_modules/${packageName}/dist`, 'src'); + + return new Request(`${this.compilation.context.projectDirectory}${themePackUrl}`); } } diff --git a/packages/cli/test/cases/theme-pack/my-theme-pack.js b/packages/cli/test/cases/theme-pack/my-theme-pack.js index 168736103..745f70710 100644 --- a/packages/cli/test/cases/theme-pack/my-theme-pack.js +++ b/packages/cli/test/cases/theme-pack/my-theme-pack.js @@ -1,15 +1,13 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath, URL } from 'url'; +import fs from 'fs/promises'; -const packageJson = JSON.parse(await fs.promises.readFile(new URL('./package.json', import.meta.url), 'utf-8')); +const packageJson = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf-8')); const myThemePack = (options = {}) => [{ type: 'context', name: `${packageJson.name}:context`, provider: (compilation) => { const templateLocation = options.__isDevelopment // eslint-disable-line no-underscore-dangle - ? path.join(compilation.context.userWorkspace, 'layouts') - : fileURLToPath(new URL('./dist/layouts', import.meta.url)); + ? new URL('./layouts/', compilation.context.userWorkspace) + : new URL('./dist/layouts/', import.meta.url); return { templates: [ diff --git a/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js b/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js index 49c4ce574..123e307aa 100644 --- a/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js +++ b/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js @@ -131,13 +131,13 @@ describe('Build Greenwood With: ', function() { expect(await glob.promise(path.join(this.context.publicDir, '**/header.*.js'))).to.have.lengthOf(1); }); - it('should have expected link tag in the head', function() { - const scriptTag = Array.from(dom.window.document.querySelectorAll('head script')) - .filter((linkTag) => { - return linkTag.getAttribute('src').indexOf('/header.') === 0; + it('should have expected script tag in the head', function() { + const scriptTags = Array.from(dom.window.document.querySelectorAll('head script')) + .filter((scriptTag) => { + return scriptTag.getAttribute('src').indexOf('/header.') === 0; }); - expect(scriptTag.length).to.equal(1); + expect(scriptTags.length).to.equal(1); }); it('should have expected component', function() { diff --git a/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js b/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js index 1667cd584..3a6f14b2e 100644 --- a/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js +++ b/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js @@ -141,7 +141,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/css'); + expect(response.headers['content-type']).to.contain('text/css'); done(); }); @@ -179,7 +179,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); diff --git a/packages/plugin-babel/src/index.js b/packages/plugin-babel/src/index.js index 364f9c051..56531c955 100644 --- a/packages/plugin-babel/src/index.js +++ b/packages/plugin-babel/src/index.js @@ -4,25 +4,24 @@ * */ import babel from '@babel/core'; -import fs from 'fs'; -import path from 'path'; +import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; import rollupBabelPlugin from '@rollup/plugin-babel'; -async function getConfig (compilation, extendConfig = false) { +async function getConfig(compilation, extendConfig = false) { const { projectDirectory } = compilation.context; const configFile = 'babel.config.mjs'; - const defaultConfig = (await import(new URL(configFile, import.meta.url).pathname)).default; - const userConfig = fs.existsSync(path.join(projectDirectory, configFile)) + const defaultConfig = (await import(new URL(`./${configFile}`, import.meta.url))).default; + const userConfig = await checkResourceExists(new URL(`./${configFile}`, projectDirectory)) ? (await import(`${projectDirectory}/${configFile}`)).default : {}; - let finalConfig = Object.assign({}, userConfig); - + const finalConfig = Object.assign({}, userConfig); + if (extendConfig) { finalConfig.presets = Array.isArray(userConfig.presets) ? [...defaultConfig.presets, ...userConfig.presets] : [...defaultConfig.presets]; - + finalConfig.plugins = Array.isArray(userConfig.plugins) ? [...defaultConfig.plugins, ...userConfig.plugins] : [...defaultConfig.plugins]; @@ -34,26 +33,21 @@ async function getConfig (compilation, extendConfig = false) { class BabelResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['.js']; + this.extensions = ['js']; this.contentType = ['text/javascript']; } async shouldIntercept(url) { - return Promise.resolve(path.extname(url) === this.extensions[0] && url.indexOf('node_modules/') < 0); + return url.pathname.split('.').pop() === this.extensions[0] && !url.pathname.startsWith('/node_modules/'); } - async intercept(url, body) { - return new Promise(async(resolve, reject) => { - try { - const config = await getConfig(this.compilation, this.options.extendConfig); - const result = await babel.transform(body, config); - - resolve({ - body: result.code - }); - } catch (e) { - reject(e); - } + async intercept(url, request, response) { + const config = await getConfig(this.compilation, this.options.extendConfig); + const body = await response.text(); + const result = await babel.transform(body, config); + + return new Response(result.code, { + headers: response.headers }); } } diff --git a/packages/plugin-google-analytics/src/index.js b/packages/plugin-google-analytics/src/index.js index 924a602f1..5e8b5d994 100644 --- a/packages/plugin-google-analytics/src/index.js +++ b/packages/plugin-google-analytics/src/index.js @@ -13,39 +13,36 @@ class GoogleAnalyticsResource extends ResourceInterface { this.contentType = 'text/html'; } - async shouldIntercept(url, body, headers) { - return Promise.resolve((headers.request.accept || '').indexOf(this.contentType) >= 0); + async shouldIntercept(url, request, response) { + return response.headers.get('Content-Type').indexOf(this.contentType) >= 0; } - async intercept(url, body) { + async intercept(url, request, response) { const { analyticsId, anonymous } = this.options; const trackAnon = typeof anonymous === 'boolean' ? anonymous : true; - - return new Promise((resolve, reject) => { - try { - const newHtml = body.replace('', ` - - - - - `); - - resolve({ body: newHtml }); - } catch (e) { - reject(e); - } + let body = await response.text(); + + body = body.replace('', ` + + + + + `); + + return new Response(body, { + headers: response.headers }); } } diff --git a/packages/plugin-google-analytics/test/cases/default/default.spec.js b/packages/plugin-google-analytics/test/cases/default/default.spec.js index 6d191c76b..4db2a6af1 100644 --- a/packages/plugin-google-analytics/test/cases/default/default.spec.js +++ b/packages/plugin-google-analytics/test/cases/default/default.spec.js @@ -79,20 +79,20 @@ describe('Build Greenwood With: ', function() { it('should have the expected code with users analyticsId', function() { const expectedContent = ` - var getOutboundLink = function(url) { - gtag('event', 'click', { - 'event_category': 'outbound', - 'event_label': url, - 'transport_type': 'beacon' - }); - } - window.dataLayer = window.dataLayer || []; - function gtag(){dataLayer.push(arguments);} - gtag('js', new Date()); - gtag('config', '${mockAnalyticsId}', { 'anonymize_ip': true }); + var getOutboundLink = function(url) { + gtag('event', 'click', { + 'event_category': 'outbound', + 'event_label': url, + 'transport_type': 'beacon' + }); + } + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '${mockAnalyticsId}', { 'anonymize_ip': true }); `; - expect(inlineScript[0].textContent).to.contain(expectedContent); + expect(inlineScript[0].textContent.trim().replace(/\n/g, '').replace(/ /g, '')).to.contain(expectedContent.trim().replace(/\n/g, '').replace(/ /g, '')); }); it('should only have one external Google script tag', function() { diff --git a/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js b/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js index 057d6c390..b3035f1c3 100644 --- a/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js +++ b/packages/plugin-google-analytics/test/cases/option-anonymous/option-anonymous.spec.js @@ -15,7 +15,7 @@ * plugins: [{ * greenwoodPluginGoogleAnalytics({ * analyticsId: 'UA-123456-1', - * anonymouse: false + * anonymous: false * }) * }] * @@ -81,7 +81,7 @@ describe('Build Greenwood With: ', function() { gtag('config', '${mockAnalyticsId}', { 'anonymize_ip': false }); `; - expect(inlineScript[0].textContent).to.contain(expectedContent); + expect(inlineScript[0].textContent.trim().replace(/\n/g, '').replace(/ /g, '')).to.contain(expectedContent.trim().replace(/\n/g, '').replace(/ /g, '')); }); }); diff --git a/packages/plugin-graphql/src/core/cache.js b/packages/plugin-graphql/src/core/cache.js index b3f070149..52cd82845 100644 --- a/packages/plugin-graphql/src/core/cache.js +++ b/packages/plugin-graphql/src/core/cache.js @@ -1,5 +1,6 @@ import ApolloCore from '@apollo/client/core/core.cjs.js'; -import fs from 'fs'; +import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js'; +import fs from 'fs/promises'; import { gql } from 'apollo-server'; import { getQueryHash } from './common.js'; @@ -28,14 +29,14 @@ const createCache = async (req, context) => { const cache = JSON.stringify(data); const queryHash = getQueryHash(query, variables); const hashFilename = `${queryHash}-cache.json`; - const cachePath = `${outputDir}/${hashFilename}`; + const cachePath = new URL(`./${hashFilename}`, outputDir); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir); + if (!await checkResourceExists(outputDir)) { + await fs.mkdir(outputDir); } - if (!fs.existsSync(cachePath)) { - fs.writeFileSync(cachePath, cache, 'utf8'); + if (!await checkResourceExists(cachePath)) { + await fs.writeFile(cachePath, cache, 'utf-8'); } } diff --git a/packages/plugin-graphql/src/index.js b/packages/plugin-graphql/src/index.js index bcf3c0f5a..0d2353bd5 100644 --- a/packages/plugin-graphql/src/index.js +++ b/packages/plugin-graphql/src/index.js @@ -1,7 +1,6 @@ -import fs from 'fs'; +import fs from 'fs/promises'; import { graphqlServer } from './core/server.js'; import { mergeImportMap } from '@greenwood/cli/src/lib/walker-package-ranger.js'; -import path from 'path'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; import { ServerInterface } from '@greenwood/cli/src/lib/server-interface.js'; import rollupPluginAlias from '@rollup/plugin-alias'; @@ -19,63 +18,53 @@ const importMap = { class GraphQLResource extends ResourceInterface { constructor(compilation, options = {}) { super(compilation, options); - this.extensions = ['.gql']; + this.extensions = ['gql']; this.contentType = ['text/javascript', 'text/html']; } + async shouldServe(url) { + return url.protocol === 'file:' && this.extensions.indexOf(url.pathname.split('.').pop()) >= 0; + } + async serve(url) { - return new Promise(async (resolve, reject) => { - try { - const js = await fs.promises.readFile(url, 'utf-8'); - const body = ` - export default \`${js}\`; - `; + const js = await fs.readFile(url, 'utf-8'); + const body = ` + export default \`${js}\`; + `; - resolve({ - body, - contentType: this.contentType[0] - }); - } catch (e) { - reject(e); - } + return new Response(body, { + headers: new Headers({ + 'Content-Type': this.contentType[0] + }) }); } - async shouldIntercept(url, body, headers) { - return Promise.resolve(headers.request.accept && headers.request.accept.indexOf(this.contentType[1]) >= 0); + async shouldIntercept(url, request, response) { + return response.headers.get('Content-Type').indexOf(this.contentType[1]) >= 0; } - async intercept(url, body) { - return new Promise(async (resolve, reject) => { - try { - const newBody = mergeImportMap(body, importMap); + async intercept(url, request, response) { + const body = await response.text(); + const newBody = mergeImportMap(body, importMap); - resolve({ body: newBody }); - } catch (e) { - reject(e); - } - }); + return new Response(newBody); } - async shouldOptimize(url = '', body, headers = {}) { - return Promise.resolve(path.extname(url) === '.html' || (headers.request && headers.request['content-type'].indexOf('text/html') >= 0)); + async shouldOptimize(url, response) { + return response.headers.get('Content-Type').indexOf(this.contentType[1]) >= 0; } - async optimize(url, body) { - return new Promise((resolve, reject) => { - try { - body = body.replace('', ` - - - `); - - resolve(body); - } catch (e) { - reject(e); - } - }); + async optimize(url, response) { + let body = await response.text(); + + body = body.replace('', ` + + + `); + + return new Response(body); } } diff --git a/packages/plugin-graphql/src/schema/schema.js b/packages/plugin-graphql/src/schema/schema.js index 15b95bc21..7f1dd0fad 100644 --- a/packages/plugin-graphql/src/schema/schema.js +++ b/packages/plugin-graphql/src/schema/schema.js @@ -1,15 +1,14 @@ +import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js'; import { makeExecutableSchema } from 'apollo-server-express'; import { configTypeDefs, configResolvers } from './config.js'; import { graphTypeDefs, graphResolvers } from './graph.js'; -import fs from 'fs'; +import fs from 'fs/promises'; import gql from 'graphql-tag'; -import path from 'path'; -import { pathToFileURL } from 'url'; const createSchema = async (compilation) => { const { graph } = compilation; const uniqueCustomDataDefKeys = {}; - const customSchemasPath = `${compilation.context.userWorkspace}/data/schema`; + const customSchemasUrl = new URL('./data/schema/', compilation.context.userWorkspace); const customUserResolvers = []; const customUserDefs = []; let customDataDefsString = ''; @@ -36,13 +35,13 @@ const createSchema = async (compilation) => { } `; - if (fs.existsSync(customSchemasPath)) { + if (await checkResourceExists(customSchemasUrl)) { console.log('custom schemas directory detected, scanning...'); - const schemaPaths = (await fs.promises.readdir(customSchemasPath)) - .filter(file => path.extname(file) === '.js'); + const schemaPaths = (await fs.readdir(customSchemasUrl)) + .filter(file => file.split('.').pop() === 'js'); for (const schemaPath of schemaPaths) { - const { customTypeDefs, customResolvers } = await import(pathToFileURL(`${customSchemasPath}/${schemaPath}`)); + const { customTypeDefs, customResolvers } = await import(new URL(`./${schemaPath}`, customSchemasUrl)); customUserDefs.push(customTypeDefs); customUserResolvers.push(customResolvers); diff --git a/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js b/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js index 54efe5778..84b016e8b 100644 --- a/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js @@ -134,7 +134,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -170,7 +170,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); diff --git a/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js b/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js index d4389f8bf..b44c12069 100644 --- a/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js +++ b/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js @@ -130,7 +130,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the expected query response', function(done) { - expect(response.body.data.config.workspace).to.equal(path.join(outputPath, 'src')); + expect(response.body.data.config.workspace).to.equal(new URL('./src/', import.meta.url).href); done(); }); }); diff --git a/packages/plugin-import-commonjs/src/index.js b/packages/plugin-import-commonjs/src/index.js index d1506296f..51fcfe897 100644 --- a/packages/plugin-import-commonjs/src/index.js +++ b/packages/plugin-import-commonjs/src/index.js @@ -4,8 +4,7 @@ * */ import commonjs from '@rollup/plugin-commonjs'; -import fs from 'fs'; -import path from 'path'; +import fs from 'fs/promises'; import { parse, init } from 'cjs-module-lexer'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; import rollupStream from '@rollup/stream'; @@ -13,12 +12,13 @@ import rollupStream from '@rollup/stream'; // bit of a workaround for now, but maybe this could be supported by cjs-module-lexar natively? // https://github.com/guybedford/cjs-module-lexer/issues/35 const testForCjsModule = async(url) => { + const { pathname } = url; let isCommonJs = false; - if (path.extname(url) === '.js' && (/node_modules/).test(url) && url.indexOf('es-module-shims.js') < 0) { + if (pathname.split('.').pop() === '.js' && pathname.startsWith('/node_modules/') && pathname.indexOf('es-module-shims.js') < 0) { try { await init(); - const body = await fs.promises.readFile(url, 'utf-8'); + const body = await fs.readFile(url, 'utf-8'); await parse(body); isCommonJs = true; @@ -26,7 +26,7 @@ const testForCjsModule = async(url) => { const { message } = e; const isProbablyLexarErrorSoIgnore = message.indexOf('Unexpected import statement in CJS module.') >= 0 || message.indexOf('Unexpected export statement in CJS module.') >= 0; - + if (!isProbablyLexarErrorSoIgnore) { // we probably _shouldn't_ ignore this, so let's log it since we don't want to swallow all errors console.error(e); @@ -34,7 +34,7 @@ const testForCjsModule = async(url) => { } } - return Promise.resolve(isCommonJs); + return isCommonJs; }; class ImportCommonJsResource extends ResourceInterface { @@ -43,23 +43,16 @@ class ImportCommonJsResource extends ResourceInterface { } async shouldIntercept(url) { - return new Promise(async (resolve, reject) => { - try { - const isCommonJs = await testForCjsModule(url); - - return resolve(isCommonJs); - } catch (e) { - console.error(e); - reject(e); - } - }); + return await testForCjsModule(url); } async intercept(url) { + const { pathname } = url; + return new Promise(async(resolve, reject) => { try { const options = { - input: url, + input: pathname, output: { format: 'esm' }, plugins: [ commonjs() @@ -70,10 +63,10 @@ class ImportCommonJsResource extends ResourceInterface { stream.on('data', (data) => (bundle += data)); stream.on('end', () => { - console.debug(`proccessed module "${url}" as a CommonJS module type.`); - resolve({ - body: bundle - }); + console.debug(`processed module "${pathname}" as a CommonJS module type.`); + resolve(new Response(bundle, { + headers: response.headers + })); }); } catch (e) { reject(e); diff --git a/packages/plugin-import-css/src/index.js b/packages/plugin-import-css/src/index.js index 8c186cb58..333b25fda 100644 --- a/packages/plugin-import-css/src/index.js +++ b/packages/plugin-import-css/src/index.js @@ -3,61 +3,47 @@ * Enables using JavaScript to import CSS files, using ESM syntax. * */ -import fs from 'fs'; -import path from 'path'; +import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; -import { pathToFileURL } from 'url'; class ImportCssResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['.css']; + this.extensions = ['css']; this.contentType = 'text/javascript'; } - // TODO resolve as part of https://github.com/ProjectEvergreen/greenwood/issues/952 - async shouldServe() { - return false; - } - // https://github.com/ProjectEvergreen/greenwood/issues/700 async shouldResolve(url) { - const isCssInDisguise = url.endsWith(this.extensions[0]) && fs.existsSync(`${url}.js`); - - return Promise.resolve(isCssInDisguise); + return url.pathname.endsWith(`.${this.extensions[0]}`) && await checkResourceExists(url); } async resolve(url) { - return Promise.resolve(`${url}.js`); + return new Request(`file://${url.pathname}.js`); } - async shouldIntercept(url, body, headers = { request: {} }) { - const { originalUrl = '' } = headers.request; - const accept = headers.request.accept || ''; - const isCssFile = path.extname(url) === this.extensions[0]; - const notFromBrowser = accept.indexOf('text/css') < 0 && accept.indexOf('application/signed-exchange') < 0; + async shouldIntercept(url, request) { + const { pathname } = url; + const accepts = request.headers.get('accept') || ''; + const isCssFile = pathname.split('.').pop() === this.extensions[0]; + const notFromBrowser = accepts.indexOf('text/css') < 0 && accepts.indexOf('application/signed-exchange') < 0; // https://github.com/ProjectEvergreen/greenwood/issues/492 - const isCssInJs = originalUrl.indexOf('?type=css') >= 0 + const isCssInJs = url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0] || isCssFile && notFromBrowser - || isCssFile && notFromBrowser && url.indexOf('/node_modules/') >= 0; + || isCssFile && notFromBrowser && pathname.startsWith('/node_modules/'); - return Promise.resolve(isCssInJs); + return isCssInJs; } - async intercept(url, body) { - return new Promise(async (resolve, reject) => { - try { - const finalBody = body || await fs.promises.readFile(pathToFileURL(url), 'utf-8'); - const cssInJsBody = `const css = \`${finalBody.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default css;`; - - resolve({ - body: cssInJsBody, - contentType: this.contentType - }); - } catch (e) { - reject(e); - } + async intercept(url, request, response) { + const body = await response.text(); + const cssInJsBody = `const css = \`${body.replace(/\r?\n|\r/g, ' ').replace(/\\/g, '\\\\')}\`;\nexport default css;`; + + return new Response(cssInJsBody, { + headers: new Headers({ + 'Content-Type': this.contentType + }) }); } } diff --git a/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js index 601c872a3..035711775 100644 --- a/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js @@ -89,7 +89,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); @@ -132,7 +132,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); diff --git a/packages/plugin-import-json/src/index.js b/packages/plugin-import-json/src/index.js index 99f0d0642..ebfff8f5e 100644 --- a/packages/plugin-import-json/src/index.js +++ b/packages/plugin-import-json/src/index.js @@ -4,45 +4,29 @@ * This is a Greenwood default plugin. * */ -import fs from 'fs'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; class ImportJsonResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['.json']; + this.extensions = ['json']; this.contentType = 'text/javascript'; } - // TODO resolve as part of https://github.com/ProjectEvergreen/greenwood/issues/952 - async shouldServe() { - return false; - } - - // TODO handle it from node_modules too, when without `?type=json` - async shouldIntercept(url, body, headers) { - const { originalUrl } = headers.request; - const type = this.extensions[0].replace('.', ''); + async shouldIntercept(url) { + const { pathname } = url; - return Promise.resolve(originalUrl && originalUrl.indexOf(`?type=${type}`) >= 0); + return pathname.split('.').pop() === this.extensions[0] && (url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0]); } - async intercept(url, body = '') { - return new Promise(async (resolve, reject) => { - try { - // TODO better way to handle this? - // https://github.com/ProjectEvergreen/greenwood/issues/948 - const raw = body === '' - ? await fs.promises.readFile(url, 'utf-8') - : body; + async intercept(url, request, response) { + const json = await response.json(); + const body = `export default ${JSON.stringify(json)}`; - resolve({ - body: `export default ${JSON.stringify(raw)}`, - contentType: this.contentType - }); - } catch (e) { - reject(e); - } + return new Response(body, { + headers: new Headers({ + 'Content-Type': this.contentType + }) }); } } diff --git a/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js index 9bb03407c..26c24e787 100644 --- a/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-import-json/test/cases/develop.default/develop.default.spec.js @@ -13,8 +13,8 @@ * * { * plugins: [{ - * ...greenwoodPluginImportJson() - * }] + * greenwoodPluginImportJson() + * }] * } * * diff --git a/packages/plugin-import-json/test/cases/develop.default/src/main.json b/packages/plugin-import-json/test/cases/develop.default/src/main.json index ba21e8844..ea0ea4f9a 100644 --- a/packages/plugin-import-json/test/cases/develop.default/src/main.json +++ b/packages/plugin-import-json/test/cases/develop.default/src/main.json @@ -1,4 +1 @@ -{ - "status": 200, - "message": "got json" -} \ No newline at end of file +{"status":200,"message":"got json"} \ No newline at end of file diff --git a/packages/plugin-include-html/src/index.js b/packages/plugin-include-html/src/index.js index 031b9bec8..12682e661 100644 --- a/packages/plugin-include-html/src/index.js +++ b/packages/plugin-include-html/src/index.js @@ -1,7 +1,5 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'fs/promises'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; -import { pathToFileURL } from 'url'; class IncludeHtmlResource extends ResourceInterface { constructor(compilation, options) { @@ -10,44 +8,42 @@ class IncludeHtmlResource extends ResourceInterface { this.contentType = 'text/html'; } - async shouldIntercept(url, body, headers) { - return Promise.resolve(headers.response['content-type'] === this.contentType); + async shouldIntercept(url, request, response) { + return response.headers.get('Content-Type').indexOf(this.contentType) >= 0; } - async intercept(url, body) { - return new Promise(async (resolve, reject) => { - try { - const includeLinksRegexMatches = body.match(//g); - const includeCustomElementssRegexMatches = body.match(/<[a-zA-Z]*-[a-zA-Z](.*)>(.*)<\/[a-zA-Z]*-[a-zA-Z](.*)>/g); - - if (includeLinksRegexMatches) { - includeLinksRegexMatches - .filter(link => link.indexOf('rel="html"') > 0) - .forEach((link) => { - const href = link.match(/href="(.*)"/)[1]; - const includeContents = fs.readFileSync(path.join(this.compilation.context.userWorkspace, href), 'utf-8'); - - body = body.replace(link, includeContents); - }); - } - - if (includeCustomElementssRegexMatches) { - const customElementTags = includeCustomElementssRegexMatches.filter(customElementTag => customElementTag.indexOf('src=') > 0); - - for (const tag of customElementTags) { - const src = tag.match(/src="(.*)"/)[1]; - const filepath = path.join(this.compilation.context.userWorkspace, this.getBareUrlPath(src.replace(/\.\.\//g, ''))); - const { getData, getTemplate } = await import(pathToFileURL(filepath)); - const includeContents = await getTemplate(await getData()); - - body = body.replace(tag, includeContents); - } - } - - resolve({ body }); - } catch (e) { - reject(e); + async intercept(url, request, response) { + let body = await response.text(); + const includeLinksRegexMatches = body.match(//g); + const includeCustomElementsRegexMatches = body.match(/<[a-zA-Z]*-[a-zA-Z](.*)>(.*)<\/[a-zA-Z]*-[a-zA-Z](.*)>/g); + + if (includeLinksRegexMatches) { + const htmlIncludeLinks = includeLinksRegexMatches.filter(link => link.indexOf('rel="html"') > 0); + + for (const link of htmlIncludeLinks) { + const href = link.match(/href="(.*)"/)[1]; + const prefix = href.startsWith('/') ? '.' : ''; + const includeContents = await fs.readFile(new URL(`${prefix}${href}`, this.compilation.context.userWorkspace), 'utf-8'); + + body = body.replace(link, includeContents); + } + } + + if (includeCustomElementsRegexMatches) { + const customElementTags = includeCustomElementsRegexMatches.filter(customElementTag => customElementTag.indexOf('src=') > 0); + + for (const tag of customElementTags) { + const src = tag.match(/src="(.*)"/)[1]; + const srcUrl = new URL(`./${src.replace(/\.\.\//g, '')}`, this.compilation.context.userWorkspace); + const { getData, getTemplate } = await import(srcUrl); + const includeContents = await getTemplate(await getData()); + + body = body.replace(tag, includeContents); } + } + + return new Response(body, { + headers: response.headers }); } } diff --git a/packages/plugin-include-html/test/cases/build.default-custom-element/src/components/footer.js b/packages/plugin-include-html/test/cases/build.default-custom-element/src/components/footer.js index f2a883938..c62e8b7de 100644 --- a/packages/plugin-include-html/test/cases/build.default-custom-element/src/components/footer.js +++ b/packages/plugin-include-html/test/cases/build.default-custom-element/src/components/footer.js @@ -1,5 +1,4 @@ -import fs from 'fs'; -import { fileURLToPath, URL } from 'url'; +import fs from 'fs/promises'; const getTemplate = async (data) => { return ` @@ -12,9 +11,8 @@ const getTemplate = async (data) => { }; const getData = async () => { - const dataPath = fileURLToPath(new URL('../../package.json', import.meta.url)); - const data = JSON.parse(await fs.promises.readFile(dataPath, 'utf-8')); - + const dataUrl = new URL('../../package.json', import.meta.url); + const data = JSON.parse(await fs.readFile(dataUrl, 'utf-8')); const { version } = data; return { version }; diff --git a/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js b/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js index b78b66b6f..68392a49b 100644 --- a/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js +++ b/packages/plugin-include-html/test/cases/build.default-link-tag/build.default.link-tag.spec.js @@ -36,7 +36,7 @@ import { fileURLToPath, URL } from 'url'; const expect = chai.expect; describe('Build Greenwood With HTML Include Plugin: ', function() { - const LABEL = 'Using Custom Element feature'; + const LABEL = 'Using Link Tag feature'; const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); const outputPath = fileURLToPath(new URL('.', import.meta.url)); let runner; diff --git a/packages/plugin-polyfills/src/index.js b/packages/plugin-polyfills/src/index.js index f45346093..e56252bd5 100644 --- a/packages/plugin-polyfills/src/index.js +++ b/packages/plugin-polyfills/src/index.js @@ -1,11 +1,11 @@ import { getNodeModulesLocationForPackage } from '@greenwood/cli/src/lib/node-modules-utils.js'; -import path from 'path'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; class PolyfillsResource extends ResourceInterface { constructor(compilation, options = {}) { super(compilation, options); + this.contentType = 'text/html'; this.options = { wc: true, dsd: false, @@ -14,56 +14,57 @@ class PolyfillsResource extends ResourceInterface { }; } - async shouldIntercept(url, body, headers = { request: {} }) { - return Promise.resolve(headers.request['content-type'] && headers.request['content-type'].indexOf('text/html') >= 0); + async shouldIntercept(url, request, response) { + const { protocol } = url; + const { wc, lit, dsd } = this.options; + const isEnabled = wc || lit || dsd; + + return isEnabled + && protocol.startsWith('http') + && response.headers.get('Content-Type').indexOf(this.contentType) >= 0; } - async intercept(url, body) { - return new Promise(async (resolve, reject) => { - try { - let newHtml = body; + async intercept(url, request, response) { + const { wc, lit, dsd } = this.options; + let body = await response.text(); - // standard WC polyfill - if (this.options.wc) { - newHtml = newHtml.replace('', ` - - - `); - } + // standard WC polyfill + if (wc) { + body = body.replace('', ` + + + `); + } - // append Lit polyfill next to make sure it comes before WC polyfill - if (this.options.lit) { - newHtml = newHtml.replace('', ` - - - `); - } + // append Lit polyfill next to make sure it comes before WC polyfill + if (lit) { + body = body.replace('', ` + + + `); + } - // lastly, Declarative Shadow DOM polyfill - if (this.options.dsd) { - newHtml = newHtml.replace('', ` - - - `); - } + // lastly, Declarative Shadow DOM polyfill + if (dsd) { + body = body.replace('', ` + + + `); + } - resolve({ body: newHtml }); - } catch (e) { - reject(e); - } - }); + return new Response(body); } } @@ -80,16 +81,17 @@ const greenwoodPluginPolyfills = (options = {}) => { const polyfillPackageName = '@webcomponents/webcomponentsjs'; const polyfillNodeModulesLocation = await getNodeModulesLocationForPackage(polyfillPackageName); const litNodeModulesLocation = await getNodeModulesLocationForPackage('lit'); + const standardPolyfills = [{ - from: path.join(polyfillNodeModulesLocation, 'webcomponents-loader.js'), - to: path.join(outputDir, 'webcomponents-loader.js') + from: new URL('./webcomponents-loader.js', new URL(`file://${polyfillNodeModulesLocation}/`)), + to: new URL('./webcomponents-loader.js', outputDir) }, { - from: path.join(polyfillNodeModulesLocation, 'bundles'), - to: path.join(outputDir, 'bundles') + from: new URL('./bundles/', new URL(`file://${polyfillNodeModulesLocation}/`)), + to: new URL('./bundles/', outputDir) }]; const litPolyfills = [{ - from: path.join(litNodeModulesLocation, 'polyfill-support.js'), - to: path.join(outputDir, 'polyfill-support.js') + from: new URL('./polyfill-support.js', new URL(`file://${litNodeModulesLocation}/`)), + to: new URL('./polyfill-support.js', outputDir) }]; return [ diff --git a/packages/plugin-postcss/src/index.js b/packages/plugin-postcss/src/index.js index 41bffa49e..fd01ce3f7 100644 --- a/packages/plugin-postcss/src/index.js +++ b/packages/plugin-postcss/src/index.js @@ -3,22 +3,21 @@ * Enable using PostCSS process for CSS files. * */ -import fs from 'fs'; -import path from 'path'; +import { checkResourceExists, normalizePathnameForWindows } from '@greenwood/cli/src/lib/resource-utils.js'; import postcss from 'postcss'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; -import { pathToFileURL } from 'url'; async function getConfig (compilation, extendConfig = false) { const { projectDirectory } = compilation.context; const configFile = 'postcss.config'; - const defaultConfig = (await import(new URL(`${configFile}.js`, import.meta.url).pathname)).default; - const userConfig = fs.existsSync(path.join(projectDirectory, `${configFile}.mjs`)) - ? (await import(pathToFileURL(path.join(projectDirectory, `${configFile}.mjs`)))).default + const defaultConfig = (await import(new URL(`./${configFile}.js`, import.meta.url))).default; + const userConfigUrl = new URL(`./${configFile}.mjs`, projectDirectory); + const userConfig = await checkResourceExists(userConfigUrl) + ? (await import(userConfigUrl)).default : {}; - let finalConfig = Object.assign({}, userConfig); + const finalConfig = Object.assign({}, userConfig); - if (userConfig && extendConfig) { + if (userConfig && extendConfig) { finalConfig.plugins = Array.isArray(userConfig.plugins) ? [...defaultConfig.plugins, ...userConfig.plugins] : [...defaultConfig.plugins]; @@ -30,38 +29,23 @@ async function getConfig (compilation, extendConfig = false) { class PostCssResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['.css']; + this.extensions = ['css']; this.contentType = ['text/css']; } - async shouldServe() { - return false; - } - - isCssFile(url) { - return path.extname(url) === '.css'; - } - async shouldIntercept(url) { - return Promise.resolve(this.isCssFile(url)); + return url.protocol === 'file:' && url.pathname.split('.').pop() === this.extensions[0]; } - async intercept(url, body) { - return new Promise(async(resolve, reject) => { - try { - const config = await getConfig(this.compilation, this.options.extendConfig); - const plugins = config.plugins || []; - const css = plugins.length > 0 - ? (await postcss(plugins).process(body, { from: url })).css - : body; - - resolve({ - body: css - }); - } catch (e) { - reject(e); - } - }); + async intercept(url, request, response) { + const config = await getConfig(this.compilation, this.options.extendConfig); + const plugins = config.plugins || []; + const body = await response.text(); + const css = plugins.length > 0 + ? (await postcss(plugins).process(body, { from: normalizePathnameForWindows(url) })).css + : body; + + return new Response(css); } } diff --git a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js b/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js index 24e042a91..4d96c71fa 100644 --- a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js +++ b/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js @@ -3,7 +3,6 @@ import { render } from '@lit-labs/ssr/lib/render-with-global-dom-shim.js'; import { Buffer } from 'buffer'; import { html } from 'lit'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; -import { pathToFileURL } from 'url'; import { Readable } from 'stream'; import { parentPort } from 'worker_threads'; @@ -21,7 +20,7 @@ async function getTemplateResultString(template) { return await streamToString(Readable.from(render(template))); } -async function executeRouteModule({ modulePath, compilation, route, label, id, prerender, htmlContents, scripts }) { +async function executeRouteModule({ moduleUrl, compilation, route, label, id, prerender, htmlContents, scripts }) { const parsedCompilation = JSON.parse(compilation); const parsedScripts = scripts ? JSON.parse(scripts) : []; const data = { @@ -31,6 +30,7 @@ async function executeRouteModule({ modulePath, compilation, route, label, id, p html: null }; + console.debug({ moduleUrl }); // prerender static content if (prerender) { for (const script of parsedScripts) { @@ -41,7 +41,7 @@ async function executeRouteModule({ modulePath, compilation, route, label, id, p data.html = await getTemplateResultString(templateResult); } else { - const module = await import(pathToFileURL(modulePath)).then(module => module); + const module = await import(moduleUrl).then(module => module); const { getTemplate = null, getBody = null, getFrontmatter = null } = module; if (module.default && module.tagName) { diff --git a/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js b/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js index 2bdf8c57f..5e2e8ab04 100644 --- a/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js +++ b/packages/plugin-renderer-lit/test/cases/build.default/src/pages/artists.js @@ -1,4 +1,4 @@ -import fs from 'fs'; +import fs from 'fs/promises'; import { html } from 'lit'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import '../components/greeting.js'; @@ -30,7 +30,7 @@ async function getTemplate(compilation, route) { } async function getBody() { - const artists = JSON.parse(await fs.promises.readFile(new URL('../../artists.json', import.meta.url), 'utf-8')); + const artists = JSON.parse(await fs.readFile(new URL('../../artists.json', import.meta.url), 'utf-8')); return html`

Lit SSR response

diff --git a/packages/plugin-renderer-puppeteer/src/plugins/resource.js b/packages/plugin-renderer-puppeteer/src/plugins/resource.js index d83b9020a..509f2faaf 100644 --- a/packages/plugin-renderer-puppeteer/src/plugins/resource.js +++ b/packages/plugin-renderer-puppeteer/src/plugins/resource.js @@ -1,4 +1,3 @@ -import path from 'path'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; class PuppeteerResource extends ResourceInterface { @@ -9,19 +8,23 @@ class PuppeteerResource extends ResourceInterface { this.contentType = 'text/html'; } - async shouldIntercept(url, body, headers = {}) { - const shouldIntercept = url.endsWith(path.sep) && headers.request && headers.request.accept.indexOf(this.contentType) >= 0; - - return process.env.__GWD_COMMAND__ === 'build' && shouldIntercept;// eslint-disable-line no-underscore-dangle + async shouldIntercept(url, request, response) { + const { protocol } = url; + + return process.env.__GWD_COMMAND__ === 'build' // eslint-disable-line no-underscore-dangle + && protocol.startsWith('http') + && response.headers.get('Content-Type').indexOf(this.contentType) >= 0; } - async intercept(url, body) { + async intercept(url, request, response) { + let body = await response.text(); + body = body.replace('', ` `); - return Promise.resolve({ body }); + return new Response(body); } } diff --git a/packages/plugin-typescript/src/index.js b/packages/plugin-typescript/src/index.js index 55d4d717d..027a642c7 100644 --- a/packages/plugin-typescript/src/index.js +++ b/packages/plugin-typescript/src/index.js @@ -3,8 +3,7 @@ * Enables using JavaScript to import TypeScript files, using ESM syntax. * */ -import fs from 'fs'; -import path from 'path'; +import fs from 'fs/promises'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; import tsc from 'typescript'; @@ -15,9 +14,9 @@ const defaultCompilerOptions = { sourceMap: true }; -function getCompilerOptions (projectDirectory, extendConfig) { +async function getCompilerOptions (projectDirectory, extendConfig) { const customOptions = extendConfig - ? JSON.parse(fs.readFileSync(path.join(projectDirectory, 'tsconfig.json'), 'utf-8')) + ? JSON.parse(await fs.readFile(new URL('./tsconfig.json', projectDirectory), 'utf-8')) : { compilerOptions: {} }; return { @@ -29,27 +28,28 @@ function getCompilerOptions (projectDirectory, extendConfig) { class TypeScriptResource extends ResourceInterface { constructor(compilation, options) { super(compilation, options); - this.extensions = ['.ts']; + this.extensions = ['ts']; this.contentType = 'text/javascript'; } - async serve(url) { - return new Promise(async (resolve, reject) => { - try { - const { projectDirectory } = this.compilation.context; - const source = await fs.promises.readFile(url, 'utf-8'); - const compilerOptions = getCompilerOptions(projectDirectory, this.options.extendConfig); + async shouldServe(url) { + const { pathname, protocol } = url; + const isTsFile = protocol === 'file:' && pathname.split('.').pop() === this.extensions[0]; + + return isTsFile || isTsFile && url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0]; + } - // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API - const result = tsc.transpileModule(source, { compilerOptions }); + async serve(url) { + const { projectDirectory } = this.compilation.context; + const source = await fs.readFile(url, 'utf-8'); + const compilerOptions = await getCompilerOptions(projectDirectory, this.options.extendConfig); + // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API + const body = tsc.transpileModule(source, { compilerOptions }).outputText; - resolve({ - body: result.outputText, - contentType: this.contentType - }); - } catch (e) { - reject(e); - } + return new Response(body, { + headers: new Headers({ + 'Content-Type': this.contentType + }) }); } } diff --git a/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js b/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js index b99d5d05d..15f741133 100644 --- a/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js @@ -82,7 +82,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/javascript'); + expect(response.headers['content-type']).to.contain('text/javascript'); done(); }); diff --git a/test/smoke-test.js b/test/smoke-test.js index 27a22fc04..ac7f8c83a 100644 --- a/test/smoke-test.js +++ b/test/smoke-test.js @@ -231,7 +231,7 @@ function serve(label) { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.equal('text/html'); + expect(response.headers['content-type']).to.contain('text/html'); done(); }); diff --git a/www/pages/docs/api-routes.md b/www/pages/docs/api-routes.md index 328dbd708..4e248c192 100644 --- a/www/pages/docs/api-routes.md +++ b/www/pages/docs/api-routes.md @@ -30,9 +30,9 @@ export async function handler(request) { const body = { message: `Hello ${name}!!!` }; return new Response(JSON.stringify(body), { - headers: { + headers: new Headers({ 'Content-Type': 'application/json' - } + }) }); } ``` diff --git a/www/pages/docs/configuration.md b/www/pages/docs/configuration.md index 0a095c42e..16d24c279 100644 --- a/www/pages/docs/configuration.md +++ b/www/pages/docs/configuration.md @@ -36,7 +36,7 @@ export default { ### Dev Server Configuration for Greenwood's development server is available using the `devServer` option. -- `extensions`: Provide an array of to watch for changes and reload the live server with. By default, Greenwood will already watch all "standard" web assets (HTML, CSS, JS, etc) it supports by default, as well as any extensions set by [resource plugins](/plugins/resource) you are using in your _greenwood.config.json_. +- `extensions`: Provide an array of extensions to watch for changes and reload the live server with. By default, Greenwood will already watch all "standard" web assets (HTML, CSS, JS, etc) it supports by default, as well as any extensions set by [resource plugins](/plugins/resource) you are using in your _greenwood.config.json_. - `hud`: The HUD option ([_head-up display_](https://en.wikipedia.org/wiki/Head-up_display)) is some additional HTML added to your site's page when Greenwood wants to help provide information to you in the browser. For example, if your HTML is detected as malformed, which could break the parser. Set this to `false` if you would like to turn it off. - `port`: Pick a different port when starting the dev server - `proxy`: A set of paths to match and re-route to other hosts. Highest specificity should go at the end. @@ -45,7 +45,7 @@ Configuration for Greenwood's development server is available using the `devServ ```js export default { devServer: { - extensions: ['.txt', '.rtf'], + extensions: ['txt', 'rtf'], port: 3000, proxy: { '/api': 'https://stage.myapp.com', @@ -220,9 +220,7 @@ Path to where all your project files will be located. Using an absolute path is Setting the workspace path to be the _www/_ folder in the current directory from where Greenwood is being run. ```js -import { fileURLToPath, URL } from 'url'; - export default { - workspace: fileURLToPath(new URL('./www', import.meta.url)) + workspace: new URL('./www', import.meta.url) }; ``` \ No newline at end of file diff --git a/www/pages/guides/theme-packs.md b/www/pages/guides/theme-packs.md index fab5c7099..b6e3da683 100644 --- a/www/pages/guides/theme-packs.md +++ b/www/pages/guides/theme-packs.md @@ -55,8 +55,6 @@ _package.json_ _my-theme-pack.js_ ```js -import { fileURLToPath } from 'url'; - const myThemePack = () => [{ type: 'context', name: 'my-theme-pack:context', @@ -65,7 +63,7 @@ const myThemePack = () => [{ templates: [ // import.meta.url will be located at _node_modules/your-package/_ // when your plugin is run in a user's project - fileURLToPath(new URL('./dist/layouts', import.meta.url)) + new URL('./dist/layouts', import.meta.url) ] }; } @@ -124,17 +122,14 @@ The main consideration needed for development is that your files won't be in _no So using our current example, our final _my-theme-pack.js_ would look like this: ```js -import path from 'path'; -import { fileURLToPath, URL } from 'url'; - const myThemePackPlugin = (options = {}) => [{ type: 'context', name: 'my-theme-pack:context', provider: (compilation) => { // you can use other directory names besides templates/ this way! const templateLocation = options.__isDevelopment - ? path.join(compilation.context.userWorkspace, 'layouts') - : fileURLToPath(new URL('dist/layouts', import.meta.url)); + ? new URL('./layouts/', compilation.context.userWorkspace) + : new URL('dist/layouts', import.meta.url); return { templates: [ @@ -156,7 +151,6 @@ Additionally, we make sure to pass the flag from above for `__isDevelopment` to // shared from another test import { myThemePackPlugin } from './my-theme-pack.js'; import fs from 'fs'; -import path from 'path'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; const packageName = JSON.parse(fs.readFileSync('./package.json', 'utf-8')).name; @@ -168,15 +162,16 @@ class MyThemePackDevelopmentResource extends ResourceInterface { } async shouldResolve(url) { + const { pathname } = url; // eslint-disable-next-line no-underscore-dangle - return Promise.resolve((process.env.__GWD_COMMAND__ === 'develop') && url.indexOf(`/node_modules/${packageName}/`) >= 0); + return process.env.__GWD_COMMAND__ === 'develop' && pathname.indexOf(`/node_modules/${packageName}/`) >= 0; } async resolve(url) { const { userWorkspace } = this.compilation.context; const filePath = this.getBareUrlPath(url).split(`/node_modules/${packageName}/dist/`)[1]; - return Promise.resolve(path.join(userWorkspace, filePath)); + return new URL(`./${filePath}`, userWorkspace, filePath); } } diff --git a/www/pages/plugins/context.md b/www/pages/plugins/context.md index b3734ff6d..cc67530d7 100644 --- a/www/pages/plugins/context.md +++ b/www/pages/plugins/context.md @@ -45,9 +45,7 @@ Your plugin might look like this: * acme-theme-pack.js * package.json */ -import { fileURLToPath } from 'url'; - -export function myCopyPlugin() { +export function myContextPlugin() { return { type: 'context', name: 'acme-theme-pack:context', @@ -55,7 +53,7 @@ export function myCopyPlugin() { return { templates: [ // when the plugin is installed import.meta.url will be /path/to/node_modules// - fileURLToPath(new URL('./dist/layouts', import.meta.url)) + new URL('./dist/layouts/', import.meta.url) ] }; } diff --git a/www/pages/plugins/copy.md b/www/pages/plugins/copy.md index da62156ee..9070e5227 100644 --- a/www/pages/plugins/copy.md +++ b/www/pages/plugins/copy.md @@ -10,11 +10,8 @@ index: 2 The copy plugin allows users to copy files around as part of the [build](/docs/#cli) command. For example, Greenwood uses this feature to copy all files in the user's _/assets/_ directory to final output directory automatically. You can use this plugin to copy single files, or entire directories. ## API -This plugin supports providing an array of "location" objects that can either be files or directories. - +This plugin supports providing an array of "paired" URL objects that can either be files or directories, by providing a `from` and `to` location. ```js -import path from 'path'; - export function myCopyPlugin() { return { type: 'copy', @@ -23,13 +20,13 @@ export function myCopyPlugin() { const { context } = compilation; return [{ - // can only copy a file to a file - from: path.join(context.userWorkspace, 'robots.txt'), - to: path.join(context.outputDir, 'robots.txt') + // copy a file + from: new URL('./robots.txt', context.userWorkspace), + to: new URL('./robots.txt', context.outputDir) }, { - // can only copy a directory to a directory - from: path.join(context.userWorkspace, 'pdfs'), - to: path.join(context.outputDir, 'pdfs') + // copy a directory (notice the trailing /) + from: new URL('./pdfs/', context.userWorkspace), + to: new URL('./pdfs/', context.outputDir) }]; } }; diff --git a/www/pages/plugins/index.md b/www/pages/plugins/index.md index f1e3eb47e..452b04784 100644 --- a/www/pages/plugins/index.md +++ b/www/pages/plugins/index.md @@ -43,7 +43,7 @@ export default { } } } - }] + ] } ``` @@ -72,7 +72,7 @@ export default { #### Context This provides access to all the input / output directories and file paths Greenwood uses to build the site and output all the generated files. Context is especially useful for copying files or writing to the build directory. -Here are paths you can get from `context`, all of which are absolute URLs: +Here are paths you can get from `context`, all of which are instances of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) and use the `file://` protocol: - `outputDir`: Where Greenwood outputs the final static site - `pagesDir`: Path to the _pages/_ directory in the user's workspace @@ -81,26 +81,7 @@ Here are paths you can get from `context`, all of which are absolute URLs: - `userTemplatesDir`: Path to the _templates/_ directory in the user's workspace - `userWorkspace`: Path to the workspace directory (_src/_ by default) - -Example using `context` to write to `publicDir` from _greenwood.config.js_ -```javascript -import fs from 'fs'; -import path from 'path'; - -export default { - - plugins: [{ - name: 'my-plugin' - type: 'resource', - provider: (compilation) => { - const { outputDir } = compilation.context; - - fs.writeFileSync(path.join(outputDir, 'robots.txt'), 'Hello World!'); - } - }] - -} -``` +> You can see a good example of this in use in our [context plugin docs](/plugins/copy/) ### Plugin Types While each API has its own documentation section on the left sidebar of this page, here is a quick overview of the current set of Plugin APIs Greenwood supports. diff --git a/www/pages/plugins/resource.md b/www/pages/plugins/resource.md index e5069b640..8464489f7 100644 --- a/www/pages/plugins/resource.md +++ b/www/pages/plugins/resource.md @@ -7,160 +7,181 @@ index: 3 ## Resource -Resource plugins allow the manipulation of files loaded through ESM. Depending on if you need to support a file with a custom extension, or to manipulate standard file extensions, Resource plugins provide the lifecycle hooks into Greenwood to do things like: -- Integrating Site Analytics (Google, Snowplow) or third snippets into generated _index.html_ pages -- Processing TypeScript into JavaScript +Resource plugins allow for the manipulation and transformation of files served and bundled by Greenwood. Whether you need to support a file with a custom extension or transform the contents of a file from one type to the other, resource plugins provide the lifecycle hooks into Greenwood to enable these customizations. Examples from Greenwood's own plugin system include: +* Minifying and bundling CSS +* Compiling TypeScript into JavaScript +* Converting vanilla CSS into ESM +* Injecting site analytics or other third party snippets into your HTML -This API is also used as part of our bundling process to "teach" Rollup how to process any non JavaScript files! +It uses standard Web APIs for facilitating these transformations such as [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). -### API (Resource Interface) +### API -> _**Note**: This API is [planning to change soon](https://github.com/ProjectEvergreen/greenwood/issues/948) as part of a general alignment within Greenwood to align the signatures of these lifecycle method to be more consistent with web standards in support of Greenwood adopting compatibility with [serverless and edge runtimes](https://github.com/ProjectEvergreen/greenwood/issues/953)_. - -Although JavaScript is loosely typed, a [resource "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/resource-interface.js) has been provided by Greenwood that you can use to start building your own resource plugins. Effectively you have to define two things: -- `extensions`: The file types your plugin will operate on -- `contentType`: A browser compatible contentType to ensure browsers correctly interpret you transformations +A [resource "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/resource-interface.js) has been provided by Greenwood that you can use to start building your own resource plugins with. ```javascript -import fs from 'fs'; -import path from 'path'; import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js'; class ExampleResource extends ResourceInterface { constructor(compilation, options = {}) { - this.compilation = compilation; - this.options = options; - this.extensions = []; - this.contentType = ''; + this.compilation = compilation; // Greenwood's compilation object + this.options = options; // any optional configuration provided by the user of your plugin + this.extensions = ['foo', 'bar']; // add custom extensions for file watching + live reload here, ex. ts for TypeScript } - // test if this plugin should change a relative URL from the browser to an absolute path on disk - // like for node_modules/ resolution. not commonly needed by most resource plugins - // return true | false - async shouldResolve(url) { - return Promise.resolve(false); - } + // lifecycles go here +} - // return an absolute path - async resolve(url) { - return Promise.resolve(url); +export function myResourcePlugin(options = {}) { + return { + type: 'resource', + name: 'my-resource-plugin', + provider: (compilation) => new ExampleResource(compilation, options) } +}; +``` - // test if this plugin should be used to process a given url / header for the browser - // ex: `