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

Generate Route CLI Command #365

Merged
merged 11 commits into from
Jan 20, 2023
Merged

Generate Route CLI Command #365

merged 11 commits into from
Jan 20, 2023

Conversation

cartogram
Copy link
Contributor

TLDR: This PR adds a new command to the CLI package that generates a templated routes modules.

To test this PR run npx shopify hydrogen generate route collection from inside the hello-world template. collection can be replaced with any of the following:

  • cart
  • product
  • collection
  • collections
  • policies
  • policy
  • robots
  • sitemap

We will add more routes (and refine these existing ones) in follow up PRs. These exist in a new skeleton template inside of this repo. You can run this template locally for convenience when developing or refining the generated routes.

When building and developing the CLI these files are copied to the cli's dist folder so that they are locally available when running the command.

An optional --adaptor flag exists to support projects that are not using the @shopify/remix-oxygen adaptor (the default).

Running the generate route command will extract the route template and put it in the user's local project. It will also format and compile it if they are using JavaScript and respects a local jsconfig and prettierconfig if either exists.

We also prompt if the file already exists (allowing users to overwrite if desired) and this can be bypassed with a --force flag.

@cartogram cartogram requested a review from pepicrft January 12, 2023 17:40
@caution-tape-bot
Copy link

We noticed that this PR either modifies or introduces usage of the dangerouslySetInnerHTML attribute, which can cause cross-site scripting (XSS) vulnerabilities when user controlled values are passed in.
We recommend reviewing your code to ensure that this is what you intended to use and that there is not a safe alternative available.

Docs are available here.

If unavoidable, we reccomend using an HTML sanitizer like DOMPurify to sanitize content before
rendering it as HTML.

If you have any questions or are unsure about how to move forward with this, ping #help-appsec and we would
be happy to help you out! cc: @Shopify/xss-extermination-squad

View the source of this rule in Services DB

@github-actions
Copy link
Contributor

We detected some changes in packages/*/package.json or packages/*/src, and there are no updates in the .changeset.
If the changes are user-facing and should cause a version bump, run npm run changeset add to track your changes and include them in the next release CHANGELOG.
If you are making simple updates to examples or documentation, you do not need to add a changeset.

Copy link
Contributor

@frandiox frandiox left a comment

Choose a reason for hiding this comment

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

Nice, thanks for all the extra stuff added!

In the demo, there was some concern about adding templates/skeleton. How about we move it to generators/skeleton instead? We just need to change a few paths and add it to turbo.json.

@cartogram
Copy link
Contributor Author

I added a few additional templates (for login, register and page), but would love some additional help getting these all together. @DavidWittness is this something you have interest in helping with?

Copy link
Contributor

@gfscott gfscott left a comment

Choose a reason for hiding this comment

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

Worked like a charm for the most part!

cart: '/cart',
product: '/products/$productHandle',
collection: '/collections/$collectionHandle',
collections: '/collections/index',
Copy link
Contributor

Choose a reason for hiding this comment

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

I tried these all out, only collections gave me any trouble:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh that makes sense cause there is no collections page 🤦‍♂️ will remove this from the options for now (adding templates will be a whole other thing).

Comment on lines 9 to 16
page: '/page/$pageHandle',
cart: '/cart',
product: '/products/$productHandle',
collection: '/collections/$collectionHandle',
collections: '/collections/index',
policies: '/policies/index',
policy: '/policies/$policyHandle',
robots: '/[robots.txt]',
sitemap: '/[sitemap.xml]',
account: ['/account/login', '/account/register'],
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's consider how and when we'd bundle these together, like you have with the account route.

For instance, you've implemented both collection and collections, to scaffold the $handle pages and the index, respectively. What are the advantages and tradeoffs of doing both at once? To me, it makes sense to just do them both in one go. If they don't want the index page it's easy to delete. Same with page(s), product(s), and polic(y/ies).

Whatever we do, let's make sure that we're consistent with how singular vs. plural option names behave!

Copy link
Contributor Author

@cartogram cartogram Jan 17, 2023

Choose a reason for hiding this comment

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

Yeah I'm okay with doing both at once and being consistent. It removes some potential confusion or mis-typing between collection and collections.

The logic was added for the account option, so this is more of a product and UX question. @benjaminsehl, @DavidWittness, or @mynameisadamf, let me know any of you guys disagree, otherwise going to move ahead with @gfscott's suggestion.

Comment on lines 38 to 41
name: 'resource',
description: `The resource to generate a route for.`,
required: true,
options: RESOURCES,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I understand why call it a resource here, but wondering if there's a more specific term that would make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, maybe route?

Copy link
Contributor

Choose a reason for hiding this comment

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

Route aligns much better with Hydrogen. I built a prototype for apps yesterday and chose route as well.

}

// We format the template content with Prettier.
// TODO use @shopify/cli-kit's format function once it supports TypeScript
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, would love to get to this. The built-in next-steps and links features in cli-kit would be a good chance to point people to the API resources that are most relevant to each route.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gfscott this comment is specifically about cli-kits file module which has a file.format method that I added ages ago, but needs a little fix before we can use it here. We can still access all of the goodies for outputting nice success messages though, and I'm hoping @mynameisadamf can help with this. If you have something specific/ or an example I can work from, happy to start there.

Copy link
Contributor

@juanpprieto juanpprieto left a comment

Choose a reason for hiding this comment

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

Tried tophating but getting 🤷🏼

  [MODULE_NOT_FOUND] require failed to load                                   │
│  /Users/juanp.prieto/code-work/2023/h2/node_modules/@shopify/cli-hydrogen/d  │
│  ist/commands/hydrogen/generate/route 

@cartogram
Copy link
Contributor Author

cartogram commented Jan 18, 2023

@juanpprieto you aren't the first to report issues with npx 😞 Try running npm run g from the hello-world template instead. For Example: npm run g -- route account and let me know if that works.

@juanpprieto
Copy link
Contributor

Update: If you have issues using npx try running npm run g from the hello-world template instead. For Example: npm run g -- route account

akshually I needed to do npm run build at the root.

Comment on lines +25 to +27
"fs-extra": "^10.1.0",
"prettier": "^2.8.2",
"typescript": "^4.8.3"
Copy link
Contributor

Choose a reason for hiding this comment

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

In the packages in the CLI we are more strict about the version ranges because breaking changes shipped as minor versions is more usual in Javascript than in other languages where it's easier for developers to catch them. I'd make the call here based on how much you trust that's not going to happen for these dependencies.


const RESOURCES = Object.keys(ROUTE_MAP);

export default class GenerateRoute extends Command {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend doing a bit of separation of concerns here and extract the business logic from the command. The motivations are two:

  • Make the logic more reusable
  • Being able to test it outside of the context of OCLIF.

It's in our roadmap to iterate on the @shopify/cli-kit's APIs to be more conventional about this.

export default class GenerateRoute extends Command {
static flags = {
path: commonFlags.path,
adapter: Flags.string({
Copy link
Contributor

Choose a reason for hiding this comment

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

I wish we had the ESLint rule here (it's coming), but I'd recommend having an environment variable for each of the flags.

{
name: 'resource',
description: `The resource to generate a route for.`,
required: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here regarding environment variables

Comment on lines 53 to 55
const isTypescript = await file.exists(
path.join(directory, 'tsconfig.json'),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend to have an in-memory representation of a Hydrogen project and have validators that run against it.

const hydrogenProject = await loadProject({from: directory})
await validateIsConfiguredForTypescript(hydrogenProject)
// or
await validate(hydrogenProject)

for (const item of [
...(Array.isArray(resourcePath) ? resourcePath : [resourcePath]),
]) {
runGenerate(item, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Who awaits this?

},
) {
const extension = typescript ? '.tsx' : '.jsx';
const distPath = new URL('../../../', import.meta.url).pathname;
Copy link
Contributor

Choose a reason for hiding this comment

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

const distDirectory = hydrogenProject.directories.dist

) {
const extension = typescript ? '.tsx' : '.jsx';
const distPath = new URL('../../../', import.meta.url).pathname;
const templatePath = path.join(distPath, 'templates', `${resource}.tsx`);
Copy link
Contributor

Choose a reason for hiding this comment

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

const templatesDirectory = hydrogenProject.directories.dist.templates

{
type: 'select',
name: 'value',
message: `The file ${path.relative(
Copy link
Contributor

Choose a reason for hiding this comment

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

We are relativizing twice. You can do path.relativize:

path.relativize(destinationPath)

It'll relativize from the working directory or the root depending on the resulting ../../../../ prefix.

// to JavaScript. We try to read the project's jsconfig.json, but if it
// doesn't exist, we use a default configuration.
if (!typescript) {
const config = (await file.exists(path.join(directory, 'jsconfig.json')))
Copy link
Contributor

Choose a reason for hiding this comment

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

A function that generates routes knowing how to set up a project for Typescript doesn't feel right:

const remixProject = loadRemixProject({fromDirectory: directory})
if (!remixProject.isSetupForTypescript()) {
  remixProject.setupForTypescript()
}

Comment on lines +137 to +144
lib: ['DOM', 'DOM.Iterable', 'ES2022'],
isolatedModules: true,
esModuleInterop: true,
resolveJsonModule: true,
target: 'ES2022',
strict: true,
allowJs: true,
forceConsistentCasingInFileNames: true,
skipLibCheck: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this content disconnected from the version of Remix the project is using? If not, I'd recommend obtaining it from the project and having a map between Remix version and TSConfig

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants