Skip to content

Commit

Permalink
Avoid circular imports for HMR (#2014)
Browse files Browse the repository at this point in the history
* Avoid circular imports to improve HMR in Vite

* Changesets

* Normalize loader function to enhance JSDoc

* Support function return types and fix SerializeForm in JS

* Changesets

* Update import path in examples

* Revert import path in non-diff examples
  • Loading branch information
frandiox authored Apr 19, 2024
1 parent 7545fad commit ac4e167
Show file tree
Hide file tree
Showing 15 changed files with 100 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-panthers-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-hydrogen': patch
---

Add `@return` JSDoc tag to functions in JavaScript projects.
22 changes: 22 additions & 0 deletions .changeset/two-geckos-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'skeleton': patch
---

To improve HMR in Vite, move the `useRootLoaderData` function from `app/root.tsx` to a separate file like `app/lib/root-data.ts`. This change avoids circular imports:

```tsx
// app/lib/root-data.ts
import {useMatches} from '@remix-run/react';
import type {SerializeFrom} from '@shopify/remix-oxygen';
import type {loader} from '~/root';

/**
* Access the result of the root loader from a React component.
*/
export const useRootLoaderData = () => {
const [root] = useMatches();
return root?.data as SerializeFrom<typeof loader>;
};
```

Import this hook from `~/lib/root-data` instead of `~/root` in your components.
2 changes: 1 addition & 1 deletion examples/analytics/app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Await, NavLink} from '@remix-run/react';
import {Suspense} from 'react';
import type {HeaderQuery} from 'storefrontapi.generated';
import type {LayoutProps} from '~/components/Layout';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';
import {unstable_useAnalytics as useAnalytics} from '@shopify/hydrogen';

type HeaderProps = Pick<LayoutProps, 'header' | 'cart' | 'isLoggedIn'>;
Expand Down
2 changes: 1 addition & 1 deletion examples/analytics/app/routes/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {CartQueryDataReturn} from '@shopify/hydrogen';
import {UNSTABLE_Analytics as Analytics, CartForm} from '@shopify/hydrogen';
import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen';
import {CartMain} from '~/components/Cart';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

export const meta: MetaFunction = () => {
return [{title: `Hydrogen | Cart`}];
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-cart-method/app/routes/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
CartLineUpdateInput,
} from '@shopify/hydrogen/storefront-api-types';
import {CartMain} from '~/components/Cart';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

export const meta: MetaFunction = () => {
return [{title: `Hydrogen | Cart`}];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Await, NavLink} from '@remix-run/react';
import {Suspense} from 'react';
import type {HeaderQuery} from 'storefrontapi.generated';
import type {LayoutProps} from './Layout';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

type HeaderProps = Pick<LayoutProps, 'header' | 'cart' | 'isLoggedIn'>;

Expand Down
2 changes: 1 addition & 1 deletion examples/legacy-customer-account-flow/app/routes/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {CartQueryDataReturn} from '@shopify/hydrogen';
import {CartForm} from '@shopify/hydrogen';
import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen';
import {CartMain} from '~/components/Cart';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

export const meta: MetaFunction = () => {
return [{title: `Hydrogen | Cart`}];
Expand Down
68 changes: 48 additions & 20 deletions packages/cli/src/lib/transpile/morph/functions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
FunctionDeclaration,
FunctionLikeDeclaration,
JSDocableNode,
VariableStatement,
Expand All @@ -20,31 +21,58 @@ export function generateFunctionDocumentation(

generateParameterDocumentation(functionNode, outputDocNode);

// Add type annotation to exported functions
// using the `export const foo: Foo = () => {}` syntax
const jsDocs = variableStatement?.getJsDocs()[0];
if (jsDocs?.getTags().length === 0) {
if (variableStatement) {
// Add type annotation to exported functions
// using the `export const foo: Foo = () => {}` syntax
const declaration = variableStatement?.getDeclarations()[0];
const type = declaration?.getType().getText();
if (type && type !== 'any' && type !== 'unknown') {
const tagName = type.startsWith('() =>') ? 'return' : 'type';

// Some generated docs try to import variables from the same file.
// E.g. `<typeof loader>` becomes `<typeof import("....").loader>`
const normalizedType = type
.replace(/^\(\)\s+=>\s+/, '')
.replace(/typeof import\("[^"]+"\)\./, 'typeof ');

const text =
normalizedType === 'SerializeFrom<typeof loader>'
? `{LoaderReturnData}` // Better alias
: `{${normalizedType}}`;

jsDocs.addTag({tagName, text});
const jsDocs = variableStatement?.getJsDocs()[0];

if (jsDocs?.getTags().length === 0) {
const type = declaration?.getType().getText();

if (isAnnotableType(type)) {
jsDocs.addTag({
tagName: type.startsWith('() =>') ? 'return' : 'type',
text: normalizeType(type),
});
}
}
} else {
// Add return type annotation to plain functions
const declaration = functionNode as FunctionDeclaration;
const jsDocs = declaration.getJsDocs()[0];

if (jsDocs?.getTags().length === 0) {
const returnType = declaration.getReturnType().getText();
if (isAnnotableType(returnType)) {
jsDocs.addTag({tagName: 'return', text: normalizeType(returnType)});
}
}
}
}

function isAnnotableType(type?: string): type is string {
return !!(
type &&
type !== 'any' &&
type !== 'unknown' &&
type !== '{}' &&
type !== 'boolean'
);
}

function normalizeType(type: string) {
// Some generated docs try to import variables from the same file.
// E.g. `<typeof loader>` becomes `<typeof import("....").loader>`
const normalizedType = type
.replace(/^\(\)\s+=>\s+/, '')
.replace(/typeof import\("[^"]+"\)\./, 'typeof ');

return normalizedType === 'SerializeFrom<any>'
? `{SerializeFrom<loader>}` // Fix inference for loaders outisde of routes
: `{${normalizedType}}`;
}

/**
* Generate @param documentation from function parameters for functionNode, storing it in docNode
*/
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/lib/transpile/morph/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,11 @@ export function generateTypeDefs(sourceFile: SourceFile, code: string) {

const knownGenerics: Record<string, string | undefined> = {
MetaFunction: 'T',
SerializeFrom: 'T',
};

typedefsFromImports.forEach((typeElements, moduleSpecifier) => {
for (const typeElement of typeElements) {
// We only use this in root.tsx and it's better to
// reuse the existing LoaderReturnData, so skip it.
if (typeElement === 'SerializeFrom') continue;

// Note: SerializeFrom also needs generic if we stop skipping it.
const hasGeneric = !!knownGenerics[typeElement];

typedefs.push(
Expand Down
2 changes: 1 addition & 1 deletion templates/skeleton/app/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {NavLink} from '@remix-run/react';
import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

export function Footer({
menu,
Expand Down
2 changes: 1 addition & 1 deletion templates/skeleton/app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Await, NavLink} from '@remix-run/react';
import {Suspense} from 'react';
import type {HeaderQuery} from 'storefrontapi.generated';
import type {LayoutProps} from './Layout';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

type HeaderProps = Pick<LayoutProps, 'header' | 'cart' | 'isLoggedIn'>;

Expand Down
11 changes: 11 additions & 0 deletions templates/skeleton/app/lib/root-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {useMatches} from '@remix-run/react';
import type {SerializeFrom} from '@shopify/remix-oxygen';
import type {loader} from '~/root';

/**
* Access the result of the root loader from a React component.
*/
export function useRootLoaderData() {
const [root] = useMatches();
return root?.data as SerializeFrom<typeof loader>;
}
17 changes: 2 additions & 15 deletions templates/skeleton/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import {useNonce} from '@shopify/hydrogen';
import {
defer,
type SerializeFrom,
type LoaderFunctionArgs,
} from '@shopify/remix-oxygen';
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {
Links,
Meta,
Outlet,
Scripts,
useMatches,
useRouteError,
useLoaderData,
ScrollRestoration,
Expand Down Expand Up @@ -58,14 +53,6 @@ export function links() {
];
}

/**
* Access the result of the root loader from a React component.
*/
export const useRootLoaderData = () => {
const [root] = useMatches();
return root?.data as SerializeFrom<typeof loader>;
};

export async function loader({context}: LoaderFunctionArgs) {
const {storefront, customerAccount, cart} = context;
const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN;
Expand Down Expand Up @@ -130,7 +117,7 @@ export default function App() {

export function ErrorBoundary() {
const error = useRouteError();
const rootData = useRootLoaderData();
const rootData = useLoaderData<typeof loader>();
const nonce = useNonce();
let errorMessage = 'Unknown error';
let errorStatus = 500;
Expand Down
6 changes: 3 additions & 3 deletions templates/skeleton/app/routes/blogs.$blogHandle._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {
return [{title: `Hydrogen | ${data?.blog.title ?? ''} blog`}];
};

export const loader = async ({
export async function loader({
request,
params,
context: {storefront},
}: LoaderFunctionArgs) => {
}: LoaderFunctionArgs) {
const paginationVariables = getPaginationVariables(request, {
pageBy: 4,
});
Expand All @@ -32,7 +32,7 @@ export const loader = async ({
}

return json({blog});
};
}

export default function Blog() {
const {blog} = useLoaderData<typeof loader>();
Expand Down
2 changes: 1 addition & 1 deletion templates/skeleton/app/routes/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {CartQueryDataReturn} from '@shopify/hydrogen';
import {CartForm} from '@shopify/hydrogen';
import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen';
import {CartMain} from '~/components/Cart';
import {useRootLoaderData} from '~/root';
import {useRootLoaderData} from '~/lib/root-data';

export const meta: MetaFunction = () => {
return [{title: `Hydrogen | Cart`}];
Expand Down

0 comments on commit ac4e167

Please sign in to comment.