Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/issue 948 web api standardization windows compat refactor #1047

9 changes: 6 additions & 3 deletions packages/cli/src/commands/build.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { bundleCompilation } from '../lifecycles/bundle.js';
import { checkResourceExists } from '../lib/resource-utils.js';
import { copyAssets } from '../lifecycles/copy.js';
import fs from 'fs';
import fs from 'fs/promises';
import { preRenderCompilationWorker, preRenderCompilationCustom, staticRenderCompilation } from '../lifecycles/prerender.js';
import { ServerInterface } from '../lib/server-interface.js';

Expand All @@ -15,8 +16,10 @@ const runProductionBuild = async (compilation) => {
? compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider(compilation)
: {};

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

if (prerender || prerenderPlugin.prerender) {
Expand Down
16 changes: 8 additions & 8 deletions packages/cli/src/commands/eject.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import fs from 'fs';
import fs from 'fs/promises';

const ejectConfiguration = async (compilation) => {
return new Promise(async (resolve, reject) => {
try {
const configFileDirUrl = new URL('../config/', import.meta.url);
const configFiles = await fs.promises.readdir(configFileDirUrl);
const configFiles = await fs.readdir(configFileDirUrl);

configFiles.forEach((configFile) => {
const from = new URL(`./${configFile}`, configFileDirUrl);
const to = new URL(`./${configFile}`, compilation.context.projectDirectory);
for (const file of configFiles) {
const from = new URL(`./${file}`, configFileDirUrl);
const to = new URL(`./${file}`, compilation.context.projectDirectory);

fs.copyFileSync(from.pathname, to.pathname);
await fs.copyFile(from, to);

console.log(`Ejected ${configFile} successfully.`);
});
console.log(`Ejected ${file} successfully.`);
}

console.debug('all configuration files ejected.');

Expand Down
42 changes: 24 additions & 18 deletions packages/cli/src/config/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'fs';
import fs from 'fs/promises';
import { checkResourceExists, normalizePathnameForWindows } from '../lib/resource-utils.js';

function greenwoodResourceLoader (compilation) {
const resourcePlugins = compilation.config.plugins.filter((plugin) => {
Expand All @@ -9,22 +10,25 @@ function greenwoodResourceLoader (compilation) {

return {
name: 'greenwood-resource-loader',
resolveId(id) {
async resolveId(id) {
const normalizedId = id.replace(/\?type=(.*)/, '');
const { userWorkspace } = compilation.context;

if ((id.indexOf('./') === 0 || id.indexOf('/') === 0) && fs.existsSync(new URL(`./${normalizedId}`, userWorkspace).pathname)) {
return new URL(`./${normalizedId}`, userWorkspace).pathname;
}
if (id.startsWith('./') || id.startsWith('/')) {
const prefix = id.startsWith('/') ? '.' : '';
const userWorkspaceUrl = new URL(`${prefix}${normalizedId}`, userWorkspace);

return null;
if (await checkResourceExists(userWorkspaceUrl)) {
return normalizePathnameForWindows(userWorkspaceUrl);
}
}
},
async load(id) {
const pathname = id.indexOf('?') >= 0 ? id.slice(0, id.indexOf('?')) : id;
const extension = pathname.split('.').pop();

if (extension !== '' && extension !== 'js') {
const url = new URL(`file://${pathname}`);
const url = new URL(`file://${pathname}?type=${extension}`);
const request = new Request(url.href);
let response = new Response('');

Expand All @@ -49,15 +53,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
Expand All @@ -76,8 +79,11 @@ 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) {
Expand All @@ -86,15 +92,15 @@ function greenwoodSyncPageResourceBundlesPlugin(compilation) {
const noop = rawAttributes && rawAttributes.indexOf('data-gwd-opt="none"') >= 0 || compilation.config.optimization === 'none';
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.pathname, contents);
await fs.writeFile(outputPath, contents);
}
}
}
Expand All @@ -107,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 => {
Expand All @@ -118,7 +124,7 @@ const getRollupConfig = async (compilation) => {
preserveEntrySignatures: 'strict', // https://github.com/ProjectEvergreen/greenwood/pull/990
input,
output: {
dir: outputDir.pathname,
dir: normalizePathnameForWindows(outputDir),
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].js',
sourcemap: true
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';

Expand Down
7 changes: 3 additions & 4 deletions packages/cli/src/lib/node-modules-utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// TODO convert this to use / return URLs
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';

// defer to NodeJS to find where on disk a package is located using import.meta.resolve
// and return the root absolute location
Expand Down Expand Up @@ -36,14 +35,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;
}
}

Expand Down
28 changes: 15 additions & 13 deletions packages/cli/src/lib/resource-interface.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs';
import { checkResourceExists } from './resource-utils.js';

class ResourceInterface {
constructor(compilation, options = {}) {
Expand All @@ -17,23 +17,25 @@ class ResourceInterface {
// * deep link route - /blog/releases/some-post
// * and a nested path in the template - ../../styles/theme.css
// so will get resolved as `${rootUrl}/styles/theme.css`
resolveForRelativeUrl(url, rootUrl) {
async resolveForRelativeUrl(url, rootUrl) {
const search = url.search || '';
let reducedUrl;

if (fs.existsSync(new URL(`.${url.pathname}`, rootUrl).pathname)) {
return new URL(`.${url.pathname}`, rootUrl);
if (await checkResourceExists(new URL(`.${url.pathname}`, rootUrl))) {
return new URL(`.${url.pathname}${search}`, rootUrl);
}

url.pathname.split('/')
.filter((segment) => segment !== '')
.reduce((acc, segment) => {
const reducedPath = url.pathname.replace(`${acc}/${segment}`, '');
const segments = url.pathname.split('/').filter(segment => segment !== '');
segments.shift();

if (reducedPath !== '' && fs.existsSync(new URL(`.${reducedPath}`, rootUrl).pathname)) {
reducedUrl = new URL(`.${reducedPath}`, rootUrl);
}
return `${acc}/${segment}`;
}, '');
for (let i = 0, l = segments.length - 1; i < l; i += 1) {
const nextSegments = segments.slice(i);
const urlToCheck = new URL(`./${nextSegments.join('/')}`, rootUrl);

if (await checkResourceExists(urlToCheck)) {
reducedUrl = new URL(`${urlToCheck}${search}`);
}
}

return reducedUrl;
}
Expand Down
51 changes: 32 additions & 19 deletions packages/cli/src/lib/resource-utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import fs from 'fs';
import fs from 'fs/promises';
import { hashString } from '../lib/hashing-utils.js';

function modelResource(context, type, src = undefined, contents = undefined, optimizationAttr = undefined, rawAttributes = undefined) {
async function modelResource(context, type, src = undefined, contents = undefined, optimizationAttr = undefined, rawAttributes = undefined) {
const { projectDirectory, scratchDir, userWorkspace } = context;
const extension = type === 'script' ? 'js' : 'css';
const windowsDriveRegex = /\/[a-zA-Z]{1}:\//;
let sourcePathURL;

if (src) {
Expand All @@ -14,25 +13,12 @@ function modelResource(context, type, src = undefined, contents = undefined, opt
? new URL(`.${src}`, userWorkspace)
: new URL(`./${src.replace(/\.\.\//g, '').replace('./', '')}`, userWorkspace);

contents = fs.readFileSync(sourcePathURL, 'utf-8');
contents = await fs.readFile(sourcePathURL, 'utf-8');
} else {
const scratchFileName = hashString(contents);

sourcePathURL = new URL(`./${scratchFileName}.${extension}`, scratchDir);
fs.writeFileSync(sourcePathURL, contents);
}

// TODO (good first issue) handle for Windows adding extra / in front of drive letter for whatever reason :(
// e.g. turn /C:/... -> C:/...
// and also URL is readonly in NodeJS??
if (windowsDriveRegex.test(sourcePathURL.pathname)) {
const driveMatch = sourcePathURL.pathname.match(windowsDriveRegex)[0];

sourcePathURL = {
...sourcePathURL,
pathname: sourcePathURL.pathname.replace(driveMatch, driveMatch.replace('/', '')),
href: sourcePathURL.href.replace(driveMatch, driveMatch.replace('/', ''))
};
await fs.writeFile(sourcePathURL, contents);
}

return {
Expand Down Expand Up @@ -65,7 +51,34 @@ function mergeResponse(destination, source) {
});
}

// On Windows, a URL with a drive letter like C:/ thinks it is a protocol and so prepends a /, e.g. /C:/
// This is fine with never fs methods that Greenwood uses, but tools like Rollupand PostCSS will need this handled manually
// https://github.com/rollup/rollup/issues/3779
function normalizePathnameForWindows(url) {
const windowsDriveRegex = /\/[a-zA-Z]{1}:\//;
const { pathname = '' } = url;

if (windowsDriveRegex.test(pathname)) {
const driveMatch = pathname.match(windowsDriveRegex)[0];

return pathname.replace(driveMatch, driveMatch.replace('/', ''));
}

return pathname;
}

async function checkResourceExists(url) {
try {
await fs.access(url);
return true;
} catch (e) {
return false;
}
}

export {
mergeResponse,
modelResource
modelResource,
normalizePathnameForWindows,
checkResourceExists
};
7 changes: 3 additions & 4 deletions packages/cli/src/lib/ssr-route-worker.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// https://github.com/nodejs/modules/issues/307#issuecomment-858729422
import { pathToFileURL } from 'url';
import { parentPort } from 'worker_threads';
import { renderToString, renderFromHTML } from 'wc-compiler';

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 data = {
template: null,
Expand All @@ -18,11 +17,11 @@ async function executeRouteModule({ modulePath, compilation, route, label, id, p

data.html = html;
} 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) {
const { html } = await renderToString(pathToFileURL(modulePath));
const { html } = await renderToString(new URL(moduleUrl));

data.body = html;
} else {
Expand Down
Loading