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

bug/issue 1151 custom imports not working for API routes and SSR pages #1152

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
**/node_modules/**
!.eslintrc.cjs
!.mocharc.js
packages/plugin-babel/test/cases/**/*main.js
packages/plugin-babel/test/cases/**/*main.js
TODO.md
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-terser": "^0.1.0",
"@web/rollup-plugin-import-meta-assets": "^1.0.0",
"acorn": "^8.0.1",
"acorn-walk": "^8.0.0",
"commander": "^2.20.0",
Expand Down
181 changes: 165 additions & 16 deletions packages/cli/src/config/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import fs from 'fs/promises';
import fs from 'fs';
import path from 'path';
import { checkResourceExists, normalizePathnameForWindows } from '../lib/resource-utils.js';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
import * as walk from 'acorn-walk';

// specifically to handle escodegen using require for package.json
// https://github.com/rollup/rollup/issues/2121
function cleanRollupId(id) {
return id.replace('\x00', '');
}

// specifically to handle escodegen and other node modules
// using require for package.json or other json files
// https://github.com/estools/escodegen/issues/455
function greenwoodJsonLoader() {
return {
name: 'greenwood-json-loader',
async load(id) {
const extension = id.split('.').pop();
const idUrl = new URL(`file://${cleanRollupId(id)}`);
const extension = idUrl.pathname.split('.').pop();

if (extension === 'json') {
const url = new URL(`file://${id}`);
const json = JSON.parse(await fs.readFile(url, 'utf-8'));
const json = JSON.parse(await fs.promises.readFile(idUrl, 'utf-8'));
const contents = `export default ${JSON.stringify(json)}`;

return contents;
Expand All @@ -33,11 +40,11 @@
return {
name: 'greenwood-resource-loader',
async resolveId(id) {
const normalizedId = id.replace(/\?type=(.*)/, '');
const normalizedId = cleanRollupId(id); // idUrl.pathname;
const { projectDirectory, userWorkspace } = compilation.context;

if (id.startsWith('.') && !id.startsWith(projectDirectory.pathname)) {
const prefix = id.startsWith('..') ? './' : '';
if (normalizedId.startsWith('.') && !normalizedId.startsWith(projectDirectory.pathname)) {
const prefix = normalizedId.startsWith('..') ? './' : '';
const userWorkspaceUrl = new URL(`${prefix}${normalizedId.replace(/\.\.\//g, '')}`, userWorkspace);

if (await checkResourceExists(userWorkspaceUrl)) {
Expand All @@ -46,11 +53,13 @@
}
},
async load(id) {
const pathname = id.indexOf('?') >= 0 ? id.slice(0, id.indexOf('?')) : id;
const idUrl = new URL(`file://${cleanRollupId(id)}`);
const { pathname } = idUrl;
const extension = pathname.split('.').pop();

if (extension !== '' && extension !== 'js') {
const url = new URL(`file://${pathname}?type=${extension}`);
// filter first for any bare specifiers
if (await checkResourceExists(idUrl) && extension !== '' && extension !== 'js') {
const url = new URL(`${idUrl.href}?type=${extension}`);
const request = new Request(url.href);
let response = new Response('');

Expand Down Expand Up @@ -116,12 +125,12 @@
compilation.resources.set(resource.sourcePathURL.pathname, {
...compilation.resources.get(resource.sourcePathURL.pathname),
optimizedFileName: fileName,
optimizedFileContents: await fs.readFile(outputPath, 'utf-8'),
optimizedFileContents: await fs.promises.readFile(outputPath, 'utf-8'),
contents
});

if (noop) {
await fs.writeFile(outputPath, contents);
await fs.promises.writeFile(outputPath, contents);
}
}
}
Expand All @@ -130,7 +139,139 @@
};
}

function getMetaImportPath(node) {
return node.arguments[0].value.split('/').join(path.sep);
}

function isNewUrlImportMetaUrl(node) {
return (
node.type === 'NewExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'URL' &&
node.arguments.length === 2 &&
node.arguments[0].type === 'Literal' &&
typeof getMetaImportPath(node) === 'string' &&
node.arguments[1].type === 'MemberExpression' &&
node.arguments[1].object.type === 'MetaProperty' &&
node.arguments[1].property.type === 'Identifier' &&
node.arguments[1].property.name === 'url'
);
}

// adapted from, and with credit to @web/rollup-plugin-import-meta-assets
// https://modern-web.dev/docs/building/rollup-plugin-import-meta-assets/
function greenwoodImportMetaUrl(compilation) {

return {
name: 'greenwood-import-meta-url',

async transform(code, id) {
const resourcePlugins = compilation.config.plugins.filter((plugin) => {
return plugin.type === 'resource';
}).map((plugin) => {
return plugin.provider(compilation);
});
const customResourcePlugins = compilation.config.plugins.filter((plugin) => {
return plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin;
}).map((plugin) => {
return plugin.provider(compilation);
});
const idUrl = new URL(`file://${cleanRollupId(id)}`);
const { pathname } = idUrl;
const extension = pathname.split('.').pop();
const urlWithType = new URL(`${idUrl.href}?type=${extension}`);
const request = new Request(urlWithType.href);
let canTransform = false;
let response = new Response(code);

// handle any custom imports or pre-processing needed before passing to Rollup this.parse
if (await checkResourceExists(idUrl) && extension !== '' && extension !== 'json') {
for (const plugin of resourcePlugins) {
if (plugin.shouldServe && await plugin.shouldServe(urlWithType, request)) {
response = await plugin.serve(urlWithType, request);
canTransform = true;
}
}

for (const plugin of resourcePlugins) {
if (plugin.shouldIntercept && await plugin.shouldIntercept(urlWithType, request, response.clone())) {
response = await plugin.intercept(urlWithType, request, response.clone());
canTransform = true;
}
}
}

if (!canTransform) {
return null;
}

const ast = this.parse(await response.text());
const assetUrls = [];
let modifiedCode = false;

// aggregate all references of new URL + import.meta.url
walk.simple(ast, {
NewExpression(node) {
if (isNewUrlImportMetaUrl(node)) {
const absoluteScriptDir = path.dirname(id);
const relativeAssetPath = getMetaImportPath(node);
const absoluteAssetPath = path.resolve(absoluteScriptDir, relativeAssetPath);
const assetName = path.basename(absoluteAssetPath);
const assetExtension = assetName.split('.').pop();

assetUrls.push({
url: new URL(`file://${absoluteAssetPath}?type=${assetExtension}`),
relativeAssetPath
});
}
}
});

for (const assetUrl of assetUrls) {
const { url } = assetUrl;
const { pathname } = url;
const { relativeAssetPath } = assetUrl;
const assetName = path.basename(pathname);
const assetExtension = assetName.split('.').pop();
const assetContents = await fs.promises.readFile(url, 'utf-8');
const name = assetName.replace(`.${assetExtension}`, '');
let bundleExtensions = ['js'];

for (const plugin of customResourcePlugins) {
if (plugin.shouldServe && await plugin.shouldServe(url)) {
const response = await plugin.serve(url);

if (response?.headers?.get('content-type') || ''.indexOf('text/javascript') >= 0) {
bundleExtensions = [...bundleExtensions, ...plugin.extensions];
}
}
}

const type = bundleExtensions.indexOf(assetExtension) >= 0
? 'chunk'
: 'asset';
const emitConfig = type === 'chunk'
? { type, id: normalizePathnameForWindows(url), name }
: { type, name: assetName, source: assetContents };
const ref = this.emitFile(emitConfig);
// handle Windows style paths
const normalizedRelativeAssetPath = relativeAssetPath.replace(/\\/g, '/');
const importRef = `import.meta.ROLLUP_FILE_URL_${ref}`;

modifiedCode = code
.replace(`'${normalizedRelativeAssetPath}'`, importRef)
.replace(`"${normalizedRelativeAssetPath}"`, importRef);
}

return {
code: modifiedCode ? modifiedCode : code,
map: null
};
}
};
}

// TODO could we use this instead?

Check warning on line 274 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment

Check warning on line 274 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment

Check warning on line 274 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment

Check warning on line 274 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment
// https://github.com/rollup/rollup/blob/v2.79.1/docs/05-plugin-development.md#resolveimportmeta
// https://github.com/ProjectEvergreen/greenwood/issues/1087
function greenwoodPatchSsrPagesEntryPointRuntimeImport() {
Expand Down Expand Up @@ -177,6 +318,7 @@
plugins: [
greenwoodResourceLoader(compilation),
greenwoodSyncPageResourceBundlesPlugin(compilation),
greenwoodImportMetaUrl(compilation),
...customRollupPlugins
],
context: 'window',
Expand Down Expand Up @@ -216,7 +358,12 @@
const input = [...compilation.manifest.apis.values()]
.map(api => normalizePathnameForWindows(new URL(`.${api.path}`, userWorkspace)));

// why is this needed?
await fs.promises.mkdir(new URL('./api/assets/', outputDir), {
recursive: true
});

// TODO should routes and APIs have chunks?

Check warning on line 366 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment

Check warning on line 366 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment

Check warning on line 366 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment

Check warning on line 366 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment
// https://github.com/ProjectEvergreen/greenwood/issues/1118
return [{
input,
Expand All @@ -227,9 +374,10 @@
},
plugins: [
greenwoodJsonLoader(),
greenwoodResourceLoader(compilation),
nodeResolve(),
commonjs(),
importMetaAssets()
greenwoodImportMetaUrl(compilation)
]
}];
};
Expand All @@ -237,7 +385,7 @@
const getRollupConfigForSsr = async (compilation, input) => {
const { outputDir } = compilation.context;

// TODO should routes and APIs have chunks?

Check warning on line 388 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment

Check warning on line 388 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment

Check warning on line 388 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment

Check warning on line 388 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment
// https://github.com/ProjectEvergreen/greenwood/issues/1118
return [{
input,
Expand All @@ -248,14 +396,15 @@
},
plugins: [
greenwoodJsonLoader(),
greenwoodResourceLoader(compilation),
// TODO let this through for lit to enable nodeResolve({ preferBuiltins: true })
// https://github.com/lit/lit/issues/449
// https://github.com/ProjectEvergreen/greenwood/issues/1118
nodeResolve({
preferBuiltins: true
}),
commonjs(),
importMetaAssets(),
greenwoodImportMetaUrl(compilation),
greenwoodPatchSsrPagesEntryPointRuntimeImport() // TODO a little hacky but works for now
],
onwarn: (errorObj) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* components/
* card.js
* counter.js
* images/
* logo.svg
* pages/
* about.md
* artists.js
Expand Down Expand Up @@ -300,6 +302,60 @@ describe('Serve Greenwood With: ', function() {
});
});

describe('Bundled image using new URL and import.meta.url', function() {
const bundledName = 'assets/logo-abb2e884.svg';
let bundledImageResponse = {};
let usersResponse = {};

before(async function() {
await new Promise((resolve, reject) => {
request.get(`${hostname}/${bundledName}`, (err, res, body) => {
if (err) {
reject();
}

bundledImageResponse = res;
bundledImageResponse.body = body;

resolve();
});
});

await new Promise((resolve, reject) => {
request.get(`${hostname}/_users.js`, (err, res, body) => {
if (err) {
reject();
}

usersResponse = res;
usersResponse.body = body;

resolve();
});
});
});

it('should return a 200 status for the image', function(done) {
expect(bundledImageResponse.statusCode).to.equal(200);
done();
});

it('should return the expected content-type for the image', function(done) {
expect(bundledImageResponse.headers['content-type']).to.equal('image/svg+xml');
done();
});

it('should return the expected body for the image', function(done) {
expect(bundledImageResponse.body.startsWith('<svg')).to.equal(true);
done();
});

it('should return the expected bundled image name inside the bundled page route', function(done) {
expect(usersResponse.body.indexOf(bundledName) >= 0).to.equal(true);
done();
});
});

describe('Serve command with 404 not found behavior', function() {
let response = {};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const logo = new URL('../images/logo.svg', import.meta.url);
const template = document.createElement('template');

template.innerHTML = `
Expand All @@ -23,6 +24,7 @@ template.innerHTML = `
</style>

<div class="card">
<img alt="logo" href="${logo.pathname}">
<slot name="title">My default title</slot>
<slot name="image"></slot>
</div>
Expand Down
Loading
Loading