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

Enhance: use html parser in preprocessor #240

Closed
wants to merge 17 commits into from

Conversation

eight04
Copy link
Contributor

@eight04 eight04 commented Mar 4, 2022

Fixes #226
Fixes #227
Fixes #233

Noticeable changes:

@@ -1,11 +1,4 @@
import { inlinePreprocessedSvelteComponent, escapeHtml, inlineSvelteComponent } from '../inlineSvelteComponent';

test('#escapeHtml', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

escapeHtml and replaceSpecialCharacters are moved to htmlParser.ts.

@@ -8,8 +8,8 @@ describe('#partialHydration', () => {
content: '<DatePicker hydrate-client={{ a: "b" }} />',
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} />`,
).toMatchInlineSnapshot(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switch to inline snapshot so it is easier to update.

Comment on lines +64 to +68
await expect(async () => {
await partialHydration.markup({
content: '<DatePicker hydrate-client="string />',
});
}).rejects.toThrow();
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 is not valid svelte syntax so I just let preprocessor throw.

@@ -50,6 +41,8 @@ export default function mountComponentsInHtml({ page, html, hydrateOptions }): s
page,
props: hydrateComponentProps,
hydrateOptions: hydrateComponentOptions,
otherAttributes: match[5],
openTagOnly: !match[6]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, we check whether the wrapper closes immediately to decide if the component has to be rendered again (i.e. shortcode component).

This doesn't work if the component renders into an empty string. Though it shouldn't harm either.

}
}
const spreadClientProps = clientProps ? ` {...(${clientProps})}` : '';
// FIXME: it should be possible to merge three attributes into one
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally, we should be able to generate simpler code in the preprocessor e.g.

`<div ejs-component=${stringifyExpression(`["${tag.name}",${clientProps},${options}]`)}>`

Then we just have to search for ejs-component to extract all information in mountComponentsInHtml.ts.

Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely. This would be a huge simplification.

}
const spreadClientProps = clientProps ? ` {...(${clientProps})}` : '';
// FIXME: it should be possible to merge three attributes into one
// FIXME: use hydrateOptions.element instead of 'div'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

hydrateOptions.element no longer works in this PR. I didn't find the document for it so I'm not sure how to implement it correctly.

If this is needed, we can try to find the element via regex:

const rx = /\belement['"]?\s*:\s*['"]([^'"]+)/i;
const element = options.match(rx)?.[1];

This will work if the options is written in the source:

hydrate-options={{element: "span"}}
hydrate-options={{"element": "span"}}

But won't work in other cases:

<script>
const o = {element: 'span'};
</script>
<Component hydrate-client hydrate-options={o} />

Copy link
Contributor

Choose a reason for hiding this comment

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

hydrate-options={{element: "span"}}
hydrate-options={{"element": "span"}}

This was the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another solution is to use a special tag e.g.

<ejswrapper ... ></ejswrapper>

And replace it with the real tag name in mountComponentsInHtml

Copy link
Contributor

Choose a reason for hiding this comment

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

@eight04 Not strongly opinionated either way. Whatever you think is best.

const repl = createReplacementString(content, tag);
s.overwrite(tag.start, tag.end, repl);
dirty = true;
}

const wrappingComponentPattern = /<([a-zA-Z]+)[^>]+hydrate-client={([^]*?})}[^/>]*>[^>]*<\/([a-zA-Z]+)>/gim;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We may not need this anymore?

@eight04
Copy link
Contributor Author

eight04 commented Mar 4, 2022

It seems that there is a linting error that requires changing dependencies:

Error:    1:1   error  'magic-string' should be listed in the project's dependencies. Run 'npm i -S magic-string' to add it                                                     import/no-extraneous-dependencies

@nickreese
Copy link
Contributor

@eight04 I am a bit unclear on how "server props" are getting passed into the SSR call on svelteComponent.ts.

);
let dirty = false;
const hydrateableComponentPattern = /<([a-zA-Z]+)[^>]+hydrate-client/gim;
const s = new MagicString(content);
Copy link
Contributor

Choose a reason for hiding this comment

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

Great work here. Huge simplification.

@nickreese
Copy link
Contributor

@eight04 looks good to me once we sort out how to get the ssrProps passed in and if this is something we want to do. I agree that there should be props that are SSR but don't need to get hydrated. This is a problem we've faced before.

@eight04
Copy link
Contributor Author

eight04 commented Mar 7, 2022

Yeah I think the current state of SSR props is not ideal. I will revert those changes. Currently it is built on svelte syntax, hence it will only work with svelte component, which against #237 (comment).

We should probably introduce a new attribute like hydrate-server={} which overwrites hydrate-client e.g.

<Foo hydrate-client={{a: 1, b: 2}} hydrate-server={{a: 2}} />
<!--
ssr will get {a: 2, b: 2}
client will get {a: 1, b: 2}
-->

Maybe to discuss ssr props further in #234.


Still WIP. Things need to be done in this PR:

  • Revert changes to SSR props.
  • Implment options.element aka <ejswrapper>.
  • Switch to ejs-mount directive.

src/Elder.ts Outdated
@@ -33,7 +33,7 @@ import {
} from './utils/types';
import createReadOnlyProxy from './utils/createReadOnlyProxy';
import workerBuild from './workerBuild';
import { inlineSvelteComponent } from './partialHydration/inlineSvelteComponent';
import { inlineComponent } from './partialHydration/inlineComponent';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rename inlineSvelteComponent to inlineComponent.

Comment on lines +41 to +42
"id": "0",
"name": "0",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems that the target ID is duplicated. Should we remove one of them?

initComponent(entry.target,selected)
}
});
}, { rootMargin: "200px",threshold: 0}) : undefined;`
: ''
}
Object.keys(par).forEach(k => {
const el = document.getElementById(k);
const el = document.querySelector(\`[ejs-id="\${k}"]\`);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use ejs-id to target the mount target.

@@ -220,6 +220,8 @@ class Page {

componentsToHydrate: IComponentToHydrate[];

getUniqueId = prepareGetUniqueId();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A new method is added. It generates ID incrementally so we don't have to deal with random ID in the test and generate reproducible build.

@@ -76,7 +76,7 @@ describe('#svelteComponent', () => {
expect(home(componentProps)).toEqual(`<div class="svelte-home">mock html output</div>`);
});

it('svelteComponent works with partial hydration of subcomponent', () => {
it.skip('svelteComponent works with partial hydration of subcomponent', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We will probably remove svelteComponent in another PR so I didn't fix these tests.

@eight04 eight04 closed this Nov 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants