Skip to content

Commit

Permalink
Merge pull request #1047 from ProjectEvergreen/feature/issue-948-web-…
Browse files Browse the repository at this point in the history
…api-standardization-win

Feature/issue 948 web api standardization windows compat refactor
  • Loading branch information
thescientist13 authored Feb 4, 2023
2 parents d785eb9 + 7e5f45b commit 65457e9
Show file tree
Hide file tree
Showing 47 changed files with 472 additions and 392 deletions.
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

0 comments on commit 65457e9

Please sign in to comment.