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

Scaffolding Logic #2

Open
brillout opened this issue Apr 14, 2022 · 16 comments
Open

Scaffolding Logic #2

brillout opened this issue Apr 14, 2022 · 16 comments
Assignees

Comments

@brillout
Copy link
Contributor

How about this:

if (import.meta.IS_PREACT) {
   // Preact variant
}
if (import.meta.IS_VUE) {
   // Vue variant
}
// etc.

The neat thing here is that we simply develop one big app that contains all variants. We can use IntelliSense, TypeScript, etc. just like a normal app.

Maybe we can remove the irrelevant if-blocks without any AST:

  1. We look for the string if (import.meta.IS_PREACT) { (we throw an error if we detect non-prettier syntax, e.g. if(import.meta.IS_PREACT){.
  2. We look for } with the same space padding. (When prettier is applied I believe this is a reliable way to get the end of the block.)

Or we use an AST, whatever seems easiest.

@cyco130 FYI (author of https://github.com/cyco130/create-vike)

@jrson83
Copy link
Owner

jrson83 commented Apr 16, 2022

This sounds interessting, but unfortunately I have worked with import.meta just in one scenario until now.
Can you please provide a more detailed example of the concept?

@brillout
Copy link
Contributor Author

import.meta doesn't matter here. It's only about replacing/removing static strings. It could be MACRO_PREACT instead of import.meta.IS_PREACT.

We could, for example, use Vite to do this. We can implement a Vite plugin that does these transformations.

In dev, our transfomer replaces these macros with true/fasle.

In prod, our transformer removes all macros and picks only one if-block.

@brillout
Copy link
Contributor Author

For example:

// renderer/_default.page.server.tsx

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
let renderToString: (element: unknown) => string
let PageShell: unknown
if (UI === 'react') {
  renderToString = (await import('react-dom/server')).renderToString
  PageShell = (await import('./PageShell.react.tsx')).PageShell
}
if (UI === 'preact') {
  renderToString = (await import('preact-render-to-string'))
  PageShell = (await import('./PageShell.preact.tsx')).PageShell
}

// We can reuse a single boilerplate code for both the react and preact variants 👌.
export function render(pageContext) {
  const { Page, pageProps } = pageContext

  const pageHtml = renderToString(
    <PageShell>
      <Page {...pageProps} />
    </PageShell>,
  )

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${dangerouslySkipEscape(pageHtml)}</div>
      </body>
    </html>`
}

(I think if (UI === 'react') is better than if (MACRO_PREACT) for better TypeScript ergonomics.)

So yes, multiple variants in one file. We can still have variant files such as PageShell.react.tsx and PageShell.preact.tsx.

The Vite transformer would prune if-blocks, so that only one if-block remains.

I'd suggest we try to prune if-blocks without AST first.

Without AST because we can then publish an npm package create-vite-plugin-ssr that contains only:

  1. The "big boilerplate" that contains all variants
  2. The variant pruning code

That way the npm package create-vite-plugin-ssr stays light.

Note that this scaffoling technology we are developing is more than just about vite-plugin-ssr.

It will enable things like https://divjoy.com/ but 10x better. Last time I checked it seemed like the author of Divjoy abandoned the project. I'm guessing because Divjoy become unmaintainable. (Scaffolding logic is not easy, as we can see in this very thread :-).)

This is exciting.

@jrson83
Copy link
Owner

jrson83 commented Apr 19, 2022

Scaffolding logic is not easy, as we can see in this very thread :-)
This is exciting.

Yeah, this is totally interessting and fun!⚡Yesterday i was researching for more then 10 hours the topics micro-frontends & single-SPA. I have read many examples and strategies about and now I can follow your thoughts.

That way the npm package create-vite-plugin-ssr stays light.

This will be great!

I optimized my code a lot, but I still want to spent some more time, switching to pnpm and setting up a new project structure. I will update the repo asap. 💯

@jrson83 jrson83 self-assigned this Apr 19, 2022
@brillout
Copy link
Contributor Author

👌

@jrson83
Copy link
Owner

jrson83 commented Apr 20, 2022

Oki, so I'm a bit stuck at the moment, cause I don't understand totally the concept you have in mind.

When I look at micro-frontend solutions like micro-app, or micro-zoe, it seems like a different approach.

When I look at create-vike, it also seems different, since it uses static content to generate a boilerplate/template.

When we use Vite with conditionally dynamic imports, I don't understand how we want to build/generate a boilerplate, which then installs, or can be used by a user. As far as I understand, if you run vite build with the conditionally dynamic imports, it will compile the source to javascript, what can't be actually used for development, since its no source code.

I have setup a kind of monorepo with the CLI-app, and a second app with vite, the "big boilerplate", but I don't know how to continue. What I was thinking, that the CLI could generate a .env file, with the selectedOptions, which then can be used as import.meta inside the boilerplate and as .env vars in vite.

2022-04-21_01h54_03

@brillout
Copy link
Contributor Author

Have a look at https://vitejs.dev/guide/api-plugin.html. Play around with it, e.g. implement a toy Vite plugin and apply it on a vanilla Vite app (without vite-plugin-ssr). Alternatively, we can also simply use import.meta.env as you suggested 👍.

We use Vite only for dev (when we work on developing the big boilerplate).

We don't use Vite to build. Instead we simply use JavaScript. For example:

// generateBoilerplate.ts

type Options = { framework: 'react' | 'preact', clientRouting: boolean }

export function generateBoilerplate(options: Options) {
  let boilerplateFiles = findBoilerplateFiles()

  boilerplateFiles = boilerplateFiles.map(file => {
    file.code = removeIfBlocks(file.code, options)
    return file
  })

  // ...
}

function findBoilerplateFiles(): { filePath: string, code: string }[] {
 // Crawl and get all the boilerplate `.ts` files
}

function removeIfBlocks(code: string, options: Options) {
   Object.entries(options).forEach(([optionName, optionValue]) => {
     const lines = code.split('\n')

     let state: 'OUTSIDE_IF_BLOCK' | 'INSIDE_IF_BLOCK' = 'OUTSIDE_IF_BLOCK'
     let whitespacePadding: null | number = null

     lines = lines.filter((line, i) => {
       const idx = lines.findIndex(`if (import.meta.env.${optionName}`)
       if (idx !== -1) {
         state = 'INSIDE_IF_BLOCK'
         whitespacePadding = idx
         return false // We always remove the if condition lines
       }

       if (
         state === 'INSIDE_IF_BLOCK' && line.trim() === '}' &&
         line.length === whitespacePadding.length + 1
       ) {
         state = 'OUTSIDE_IF_BLOCK'
         whitespacePadding = null
         return false
       }

       // We keep the lines that are outside the if-block.
       if (state === 'OUTSIDE_IF_BLOCK') {
         return true
       }

       if (state === 'INSIDE_IF_BLOCK') {
         // TODO: only remove if value is `!== optionValue`: if the value is `=== optionValue`
         // we should keep the block content.
         return false
       }
     })

     code = lines.join('\n')
   })

   // We should have no `import.meta.env` left after we removed the if-blocks. (We remove the
   // if-condition for the one if-block we keep.)
   assert(!code.includes('import.meta.env'), "Wrong `import.meta.env` syntax. Make sure to apply prettier.")

   return code
}

AFAICT we don't need an AST.

@jrson83
Copy link
Owner

jrson83 commented Apr 21, 2022

I edited this post, since I remember you said we can have single files for the components e.g.

index.page.preact.tsx
index.page.react.tsx
index.page.vue

I think we even must do this, since its not possible to dynamic import hooks inside a condition.

// TypeError: useState is not a function or its return value is not iterable
let useState: any
// OR
let useState: (element: any) => any

if (import.meta.env.VITE_APP_FRAMEWORK === 'React') {
  useState = (await import('react')).useState
}

So all good, I will continue tomorrow.

@brillout
Copy link
Contributor Author

💯

@jrson83
Copy link
Owner

jrson83 commented Apr 24, 2022

Today was very successful. I'll clean up the code and update the repo for sure if I'm ready.
As long, I couldn't get your pruning code example to work with array.IndexOd and wildcards, but with line.includes.

lines.findIndex(`if (import.meta.env.${optionName}`)

Nevermind, to keep this up to date here is the working code snippet I got so far, with a test string. 😎
If you have any ideas how to optimize let me know.

import assert from 'assert'

const frameworks = ['Preact', 'React', 'Vue'] as const

type Frameworks = typeof frameworks[number]

type Framework = Partial<Frameworks>

type Options = { VITE_APP_FRAMEWORK: Framework }

const VITE_APP_FRAMEWORK = 'React'

const options: Options = {
  VITE_APP_FRAMEWORK
}

const rExp = new RegExp(VITE_APP_FRAMEWORK, 'm') as unknown as Framework

generateBoilerplate(options)

export function generateBoilerplate(options: Options) {
  let boilerplateFiles = findBoilerplateFiles()

  boilerplateFiles = boilerplateFiles.map((file) => {
    file.code = removeIfBlocks(file.code, options)
    return file
  })

  // ...
}

function findBoilerplateFiles(): { filePath: string; code: string }[] {
  return [
    {
      filePath: `./boilerplate.ts`,
      code: `if (import.meta.env.VITE_APP_FRAMEWORK === 'React') {
  console.log('SELECTED REACT')
}
if (import.meta.env.VITE_APP_FRAMEWORK === 'Vue') {
  console.log('SELECTED VUE')
}
if (import.meta.env.VITE_APP_FRAMEWORK === 'Preact') {
  console.log('SELECTED PREACT')
}
console.log('END1')
console.log('END2')
console.log('END3')`
    }
  ]
}

function removeIfBlocks(code: string, options: Options) {
  Object.entries(options).forEach(([optionName, optionValue]) => {
    let lines = code.split('\n')

    let state: 'OUTSIDE_IF_BLOCK' | 'INSIDE_IF_BLOCK' = 'OUTSIDE_IF_BLOCK'
    let whitespacePadding: null | number = null
    let frameworkValue: any = null

    lines = lines.filter((line, i) => {
      const idx = line.includes(`if (import.meta.env.${optionName}`)

      if (idx) {
        frameworkValue = line.match(rExp)
        state = 'INSIDE_IF_BLOCK'
        whitespacePadding = i
        return false // We always remove the if condition lines
      }

      if (state === 'INSIDE_IF_BLOCK' && line.trim() === '}' && i === whitespacePadding + 2) {
        state = 'OUTSIDE_IF_BLOCK'
        whitespacePadding = null
        return false
      }

      // We keep the lines that are outside the if-block.
      if (state === 'OUTSIDE_IF_BLOCK') {
        return true
      }

      if (state === 'INSIDE_IF_BLOCK') {
        // TODO: only remove if value is `!== optionValue`: if the value is `=== optionValue`
        // we should keep the block content.
        return frameworkValue !== null && frameworkValue[0] === optionValue
      }
    })

    code = lines.join('\n')
  })

  console.log(code)

  // We should have no `import.meta.env` left after we removed the if-blocks. (We remove the
  // if-condition for the one if-block we keep.)
  assert(!code.includes('import.meta.env'), 'Wrong `import.meta.env` syntax. Make sure to apply prettier.')

  return code
}

@brillout
Copy link
Contributor Author

Looks good. Can't wait to see it all come together 👀.

@jrson83
Copy link
Owner

jrson83 commented Apr 26, 2022

I updated the repo.
Really like the CLI code style now! 😍 With the reducer it's sweet.
Next I integrate the generator & optimize the boilerplate package, cause that's a bit dirty. 😇

@brillout
Copy link
Contributor Author

Nice, will have a look at it later today.

Ok 👌.

@jrson83
Copy link
Owner

jrson83 commented May 10, 2022

New commit is done! Still some stuff todo ⚡

@brillout
Copy link
Contributor Author

⚡⚡⚡Neat neat neat.

:-).

All-in-all it's great. Only thing:

  • I don't know if we need this <TaskList/> thing. Although I really like when the CLI is showing concurrent loading icons. Looks cute, neat, clean, and powerful. Just wondering whether we are overengineering here. But if we do have slow tasks (e.g. running pnpm install on behalf of the user), then yea it does make sense.

As said in PM, I love many details about your code.

It's interesting that you put the reducers along TypeScript types in types.ts. Didn't like it at first, but actually it kind of makes sense. Looking forward to see how it's going to evolve.

@jrson83
Copy link
Owner

jrson83 commented May 12, 2022

Awesome that you like it 😄

I don't know if we need this thing. Although I really like when the CLI is showing concurrent loading icons. Looks cute, neat, clean, and powerful. Just wondering whether we are overengineering here. But if we do have slow tasks (e.g. running pnpm install on behalf of the user), then yea it does make sense.

I was thinking the same while developing, but realized when I replaced the delay placeholder with real promises. We will see if I add more tasks like detype, which might take longer, or like you say, if we might add a install dependencies option, it will make sense. One thing this is really cool for, when a promise is rejected, so an error happens, you can see it in the progress with icon (I will also add an error message when this happens, in the completed message as next).

This is really making fun, I will continue afap.

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

No branches or pull requests

2 participants