Skip to content

Add style preprocessing#69

Merged
natemoo-re merged 12 commits intomainfrom
feat/preprocess
Oct 20, 2021
Merged

Add style preprocessing#69
natemoo-re merged 12 commits intomainfrom
feat/preprocess

Conversation

@natemoo-re
Copy link
Member

@natemoo-re natemoo-re commented Oct 19, 2021

Changes

  • This exposes a preprocessStyle function to the compiler to preprocess styles.
  • Currently, this function has to be sync because async in Go turns out to be pretty weird. I tried.

Here are some handy Go + WASM resources specifically around Promises (that may or may not be useful)

Testing

We don't have automatic WASM tests yet, but we should think about adding them!

Docs

N/A

@changeset-bot
Copy link

changeset-bot bot commented Oct 19, 2021

🦋 Changeset detected

Latest commit: c6e6781

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@astrojs/compiler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment on lines +178 to +176
// Create and return the Promise object
promiseConstructor := js.Global().Get("Promise")
return promiseConstructor.New(handler)
Copy link
Member Author

Choose a reason for hiding this comment

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

Small refactor to return a Promise from transform.

Comment on lines +132 to +133
// Hoist styles and scripts to the top-level
transform.ExtractScriptsAndStyles(doc)
Copy link
Member Author

Choose a reason for hiding this comment

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

Main difference here is that we're splitting up the transform step into preprocessing and postprocessing.

Comment on lines +135 to +148
// Pre-process styles
// Important! These goroutines need to be spawned from this file or they don't work
var wg sync.WaitGroup
if len(doc.Styles) > 0 {
if transformOptions.PreprocessStyle.IsUndefined() != true {
for i, style := range doc.Styles {
wg.Add(1)
i := i
go preprocessStyle(i, style, transformOptions, wg.Done)
}
}
}
// Wait for all the style goroutines to finish
wg.Wait()
Copy link
Member Author

Choose a reason for hiding this comment

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

This spawns workers that run the async preprocessStyle functions and waits until they are all complete to continue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that this is the preprocessStyle function in Go which has a different signature. It safely invokes the JS preprocessStyle.

Comment on lines +150 to +151
// Perform CSS and element scoping as needed
transform.Transform(doc, transformOptions)
Copy link
Member Author

Choose a reason for hiding this comment

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

At this point, our CSS is (presumably) actually CSS and we can scope it!

Comment on lines +30 to +41
func GetAttrs(n *astro.Node) js.Value {
attrs := js.Global().Get("Object").New()
for _, attr := range n.Attr {
switch attr.Type {
case astro.QuotedAttribute:
attrs.Set(attr.Key, attr.Val)
case astro.EmptyAttribute:
attrs.Set(attr.Key, true)
}
}
return attrs
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This function returns compile-time known attributes as a JS object, which can then be passed to the preprocessors.

Comment on lines +5 to +31
// See https://stackoverflow.com/questions/68426700/how-to-wait-a-js-async-function-from-golang-wasm
func Await(awaitable js.Value) ([]js.Value, []js.Value) {
then := make(chan []js.Value)
defer close(then)
thenFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
then <- args
return nil
})
defer thenFunc.Release()

catch := make(chan []js.Value)
defer close(catch)
catchFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
catch <- args
return nil
})
defer catchFunc.Release()

awaitable.Call("then", thenFunc).Call("catch", catchFunc)

select {
case result := <-then:
return result, nil
case err := <-catch:
return nil, err
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is some magic I don't fully understand, borrowed from StackOverflow. But it works the same as await in JS!

Copy link
Member

Choose a reason for hiding this comment

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

lol I'm both scared and thrilled!

sourcefile?: string;
sourcemap?: boolean | 'inline' | 'external' | 'both';
as?: 'document'|'fragment';
preprocessStyle: (content: string, attrs: Record<string, string>) => Promise<string>
Copy link
Member Author

Choose a reason for hiding this comment

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

That's it, that's the feature!

Comment on lines +31 to +39
preprocessStyle: async (value, attrs) => {
let x = i++;
if (!attrs.lang) {
return null;
}
console.log(`Starting to preprocess style ${x} as ${attrs.lang}`);
await sleep(3000);
console.log(`Finished preprocessing ${x}`);
return value.replace('color', 'background');
Copy link
Member Author

Choose a reason for hiding this comment

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

Should be very straightforward.

preprocessStyle is an async function that gets the contents of the style element and any compile-time known attrs (we only care about lang now, but maybe others in the future!)

Simulating some expensive work with sleep here.

@FredKSchott
Copy link
Member

I'll leave for others to review, but if this works and we understand how it works, then this sounds super cool!

@matthewp
Copy link
Contributor

The CI error makes me worried that this is using something not supported by tinygo but I presume this was tested against using tinygo, right? I which case I don't know what that error is about...

Anyways, clever approach, I hope it works out!

Comment on lines +5 to +6
"GOOS": "js",
"GOARCH": "wasm",
Copy link
Member Author

Choose a reason for hiding this comment

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

Ayyyy no more VS Code warnings for using WASM

func preprocessStyle(i int, style *astro.Node, transformOptions transform.TransformOptions, cb func()) {
defer cb()
attrs := wasm_utils.GetAttrs(style)
data, _ := wasm_utils.Await(transformOptions.PreprocessStyle.(js.Value).Invoke(style.FirstChild.Data, attrs))
Copy link
Member Author

Choose a reason for hiding this comment

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

This took me a bit to figure out. PreprocessStyle is typed as interface{} which is very similar to TypeScript's any (but a little more strict). The transformOptions.PreprocessStyle.(js.Value) part casts the type as js.Value so we can access methods on that type.

@@ -0,0 +1,48 @@
package wasm_utils
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved all of the wasm_utils to their own internal_wasm directory because we can't test them directly (easily, at least)

@matthewp
Copy link
Contributor

lgtm

@natemoo-re natemoo-re merged commit 4410c5a into main Oct 20, 2021
@natemoo-re natemoo-re deleted the feat/preprocess branch October 20, 2021 16:59
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.

3 participants