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

fix: add missing fs method rewrites to handle fetchRemoteFile in dsg/ssr engine #38822

Merged
merged 4 commits into from
Jan 25, 2024
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
2 changes: 1 addition & 1 deletion e2e-tests/adapters/cypress/e2e/remote-file.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const configs = [
{
title: `remote-file (SSR, Page Query)`,
pagePath: `/routes/ssr/remote-file/`,
placeholders: false,
placeholders: true,
Copy link
Contributor Author

@pieh pieh Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was disabled because of problems that this PR is fixing. In context of this test those problems weren't fatal, however it meant that image placeholders were just not working.

The fix in this PR also solves other problems than just placeholders, but this was existing test that already we had

},
]

Expand Down
26 changes: 14 additions & 12 deletions e2e-tests/adapters/src/pages/routes/ssr/remote-file.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from "react"
import { GatsbyImage } from "gatsby-plugin-image"
import Layout from "../../../components/layout"

const RemoteFile = ({ data }) => {
const RemoteFile = ({ data, serverData }) => {
return (
<Layout>
{data.allMyRemoteImage.nodes.map(node => {
Expand Down Expand Up @@ -54,6 +54,7 @@ const RemoteFile = ({ data }) => {
</div>
)
})}
<pre>{JSON.stringify(serverData, null, 2)}</pre>
</Layout>
)
}
Expand All @@ -65,8 +66,7 @@ export const pageQuery = graphql`
id
url
filename
# FILE_CDN is not supported in SSR/DSG yet
# publicUrl
publicUrl
resize(width: 100) {
height
width
Expand All @@ -75,23 +75,17 @@ export const pageQuery = graphql`
fixed: gatsbyImage(
layout: FIXED
width: 100
# only NONE placeholder is supported in SSR/DSG
# placeholder: DOMINANT_COLOR
placeholder: NONE
placeholder: DOMINANT_COLOR
)
constrained: gatsbyImage(
layout: CONSTRAINED
width: 300
# only NONE placeholder is supported in SSR/DSG
# placeholder: DOMINANT_COLOR
placeholder: NONE
placeholder: BLURRED
)
constrained_traced: gatsbyImage(
layout: CONSTRAINED
width: 300
# only NONE placeholder is supported in SSR/DSG
# placeholder: DOMINANT_COLOR
placeholder: NONE
placeholder: TRACED_SVG
)
full: gatsbyImage(layout: FULL_WIDTH, width: 500, placeholder: NONE)
}
Expand All @@ -109,3 +103,11 @@ export const pageQuery = graphql`
`

export default RemoteFile

export function getServerData() {
return {
props: {
ssr: true,
},
}
}
Comment on lines +107 to +113
Copy link
Contributor Author

@pieh pieh Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page wasn't actually SSR... Making it SSR uncovered some other problems that this PR is also fixing

5 changes: 2 additions & 3 deletions packages/gatsby-adapter-netlify/src/file-cdn-url-generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import crypto from "crypto"
import { createHash } from "crypto"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused some problems with

Cannot find module '@babel/runtime/helpers/interopRequireDefault'

in lambdas, so easiest way was just to avoid having babel adding that

import { basename } from "path"

import type { FileCdnUrlGeneratorFn, FileCdnSourceImage } from "gatsby"
Expand All @@ -21,8 +21,7 @@ export const generateFileUrl: FileCdnUrlGeneratorFn = function generateFileUrl(
baseURL.searchParams.append(`cd`, source.internal.contentDigest)
} else {
baseURL = new URL(
`${placeholderOrigin}${pathPrefix}/_gatsby/file/${crypto
.createHash(`md5`)
`${placeholderOrigin}${pathPrefix}/_gatsby/file/${createHash(`md5`)
.update(source.url)
.digest(`hex`)}/${basename(source.filename)}`
)
Expand Down
8 changes: 7 additions & 1 deletion packages/gatsby/src/schema/graphql-engine/bundle-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ export async function createGraphqlEngineBundle(
reporter: Reporter,
isVerbose?: boolean
): Promise<webpack.Compilation | undefined> {
const state = store.getState()
const pathPrefix = state.program.prefixPaths
? state.config.pathPrefix ?? ``
: ``

const schemaSnapshotString = await fs.readFile(
path.join(rootDir, `.cache`, `schema.gql`),
`utf-8`
Expand All @@ -151,7 +156,7 @@ export async function createGraphqlEngineBundle(

// We force a specific lmdb binary module if we detected a broken lmdb installation or if we detect the presence of an adapter
let forcedLmdbBinaryModule: string | undefined = undefined
if (store.getState().adapter.instance) {
if (state.adapter.instance) {
forcedLmdbBinaryModule = `${lmdbPackage}/node.abi83.glibc.node`
}
// We always force the binary if we've installed an alternative path
Expand Down Expand Up @@ -317,6 +322,7 @@ export async function createGraphqlEngineBundle(
"process.env.GATSBY_SKIP_WRITING_SCHEMA_TO_FILE": `true`,
"process.env.NODE_ENV": JSON.stringify(`production`),
SCHEMA_SNAPSHOT: JSON.stringify(schemaSnapshotString),
PATH_PREFIX: JSON.stringify(pathPrefix),
"process.env.GATSBY_LOGGER": JSON.stringify(`yurnalist`),
"process.env.GATSBY_SLICES": JSON.stringify(
!!process.env.GATSBY_SLICES
Expand Down
22 changes: 22 additions & 0 deletions packages/gatsby/src/schema/graphql-engine/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,34 @@ export class GraphQLEngine {
this.getRunner()
}

private setupPathPrefix(pathPrefix: string): void {
if (pathPrefix) {
store.dispatch({
type: `SET_PROGRAM`,
payload: {
prefixPaths: true,
},
})

store.dispatch({
type: `SET_SITE_CONFIG`,
payload: {
...store.getState().config,
pathPrefix,
},
})
}
}

Comment on lines +46 to +64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed for pathPrefix to be actually honored when generating FILE_CDN urls

private async _doGetRunner(): Promise<GraphQLRunner> {
await tracerReadyPromise

const wrapActivity = reporter.phantomActivity(`Initializing GraphQL Engine`)
wrapActivity.start()
try {
// @ts-ignore PATH_PREFIX is being "inlined" by bundler
this.setupPathPrefix(PATH_PREFIX)

// @ts-ignore SCHEMA_SNAPSHOT is being "inlined" by bundler
store.dispatch(actions.createTypes(SCHEMA_SNAPSHOT))

Expand Down
121 changes: 114 additions & 7 deletions packages/gatsby/src/utils/page-ssr-module/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { promisify } from "util"

import type { IGatsbyPage } from "../../internal"
import type { ISSRData } from "./entry"
import { link } from "linkfs"
import { link, rewritableMethods as linkRewritableMethods } from "linkfs"

const cdnDatastore = `%CDN_DATASTORE_PATH%`
const PATH_PREFIX = `%PATH_PREFIX%`
Expand All @@ -30,7 +30,7 @@ function setupFsWrapper(): string {
global.__GATSBY.root = TEMP_DIR

// TODO: don't hardcode this
const cacheDir = `/var/task/.cache`
const cacheDir = `${process.cwd()}/.cache`

// we need to rewrite fs
const rewrites = [
Expand All @@ -47,25 +47,132 @@ function setupFsWrapper(): string {
to: TEMP_CACHE_DIR,
rewrites,
})

// copied from https://github.com/streamich/linkfs/blob/master/src/index.ts#L126-L142
function mapPathUsingRewrites(fsPath: fs.PathLike): string {
let filename = path.resolve(String(fsPath))
for (const [from, to] of rewrites) {
if (filename.indexOf(from) === 0) {
const rootRegex = /(?:^[a-zA-Z]:\\$)|(?:^\/$)/ // C:\ vs /
const isRoot = from.match(rootRegex)
const baseRegex = `^(` + from.replace(/\\/g, `\\\\`) + `)`

if (isRoot) {
const regex = new RegExp(baseRegex)
filename = filename.replace(regex, () => to + path.sep)
} else {
const regex = new RegExp(baseRegex + `(\\\\|/|$)`)
filename = filename.replace(regex, (_match, _p1, p2) => to + p2)
}
}
}
return filename
}

function applyRename<
T = typeof import("fs") | typeof import("fs").promises
>(fsToRewrite: T, lfs: T, method: "rename" | "renameSync"): void {
const original = fsToRewrite[method]
if (original) {
// @ts-ignore - complains about __promisify__ which doesn't actually exist in runtime
lfs[method] = (
...args: Parameters<typeof import("fs")["rename" | "renameSync"]>
): ReturnType<typeof import("fs")["rename" | "renameSync"]> => {
args[0] = mapPathUsingRewrites(args[0])
args[1] = mapPathUsingRewrites(args[1])
// @ts-ignore - can't decide which signature this is, but we just preserve the original
// signature here and mostly care about first 2 arguments being PathLike
return original.apply(fsToRewrite, args)
}
}
}

// linkfs doesn't handle following methods, so we need to add them manually
linkRewritableMethods.push(`createWriteStream`, `createReadStream`, `rm`)

function createLinkedFS<
T = typeof import("fs") | typeof import("fs").promises
>(fsToRewrite: T): T {
const linkedFS = link(fsToRewrite, rewrites) as T

// linkfs doesn't handle `to` argument in `rename` and `renameSync` methods
applyRename(fsToRewrite, linkedFS, `rename`)
applyRename(fsToRewrite, linkedFS, `renameSync`)

return linkedFS
}

// Alias the cache dir paths to the temp dir
const lfs = link(fs, rewrites) as typeof import("fs")
const lfs = createLinkedFS(fs)

// linkfs doesn't pass across the `native` prop, which graceful-fs needs
for (const key in lfs) {
if (Object.hasOwnProperty.call(fs[key], `native`)) {
lfs[key].native = fs[key].native
}
}

const dbPath = path.join(TEMP_CACHE_DIR, `data`, `datastore`)

// 'promises' is not initially linked within the 'linkfs'
// package, and is needed by underlying Gatsby code (the
// @graphql-tools/code-file-loader)
lfs.promises = link(fs.promises, rewrites)
lfs.promises = createLinkedFS(fs.promises)

const originalWritesStream = fs.WriteStream
function LinkedWriteStream(
this: fs.WriteStream,
...args: Parameters<(typeof fs)["createWriteStream"]>
): fs.WriteStream {
args[0] = mapPathUsingRewrites(args[0])
args[1] =
typeof args[1] === `string`
? {
flags: args[1],
// @ts-ignore there is `fs` property in options doh (https://nodejs.org/api/fs.html#fscreatewritestreampath-options)
fs: lfs,
}
: {
...(args[1] || {}),
// @ts-ignore there is `fs` property in options doh (https://nodejs.org/api/fs.html#fscreatewritestreampath-options)
fs: lfs,
}

// @ts-ignore TS doesn't like extending prototype "classes"
return originalWritesStream.apply(this, args)
}
LinkedWriteStream.prototype = Object.create(originalWritesStream.prototype)
// @ts-ignore TS doesn't like extending prototype "classes"
lfs.WriteStream = LinkedWriteStream

const originalReadStream = fs.ReadStream
function LinkedReadStream(
this: fs.ReadStream,
...args: Parameters<(typeof fs)["createReadStream"]>
): fs.ReadStream {
args[0] = mapPathUsingRewrites(args[0])
args[1] =
typeof args[1] === `string`
? {
flags: args[1],
// @ts-ignore there is `fs` property in options doh (https://nodejs.org/api/fs.html#fscreatewritestreampath-options)
fs: lfs,
}
: {
...(args[1] || {}),
// @ts-ignore there is `fs` property in options doh (https://nodejs.org/api/fs.html#fscreatewritestreampath-options)
fs: lfs,
}

// @ts-ignore TS doesn't like extending prototype "classes"
return originalReadStream.apply(this, args)
}
LinkedReadStream.prototype = Object.create(originalReadStream.prototype)
// @ts-ignore TS doesn't like extending prototype "classes"
lfs.ReadStream = LinkedReadStream

const dbPath = path.join(TEMP_CACHE_DIR, `data`, `datastore`)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like there's some repetition here with extending fs.WriteStream and fs.ReadStream, you could consider extracting/reusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's basically the same thing twice (just once for WriteStream and once for ReadStream - I think it's confusing as it is already and trying to create reusable utility would probably make debugging / understanding it worse than it already is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing class ... extends would be much more readable, but you can't actually do that is "class" you extend is prototype one like fs.WriteStream and fs.ReadStream are ( https://github.com/nodejs/node/blob/6b6bcee747c2007117262efb2ff6d61ea888f499/lib/internal/fs/streams.js#L321-L398 being one of them - the other is also in that module)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I'm ok leaving as is.

// Gatsby uses this instead of fs if present
// eslint-disable-next-line no-underscore-dangle
// @ts-ignore __promisify__ stuff
global._fsWrapper = lfs

if (!cdnDatastore) {
Expand Down