-
Notifications
You must be signed in to change notification settings - Fork 506
Importing component scripts (FR #1457) #1513
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great already. Left a few comments, but mostly nits.
I've noticed one problem: if you paste the following document into the playground it'll also import the amp-lightbox-gallery component.
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<title>My AMP Page</title>
<link rel="canonical" href="self.html" />
<meta name="viewport" content="width=device-width,minimum-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
<style amp-custom>
h1 {
margin: 1rem;
}
</style>
</head>
<body>
<h1>Hello AMPHTML World!</h1>
<amp-carousel layout=fill>
</amp-carousel>
<amp-iframe src="https://example.com" layout=fill></amp-iframe>
</body>
</html>
playground/serve.go
Outdated
func InitPlayground() { | ||
http.HandleFunc("/playground/fetch", handler) | ||
http.HandleFunc("/playground/amp-component-versions", components) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extract the playground path prefix /playground
into a constant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
playground/serve.go
Outdated
return false, err | ||
} | ||
addComponentsToStores(ctx, list) | ||
return true, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the bool return value as it's not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
playground/serve.go
Outdated
return true, nil | ||
} | ||
|
||
func getComponentsFromDataStore(ctx context.Context) (*AmpComponentsList, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/get/fetch
get implies returning a value that's already there, whereas fetch indicates that it's potentially a long running operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
playground/serve.go
Outdated
|
||
func getComponentsFromDataStore(ctx context.Context) (*AmpComponentsList, error) { | ||
log.Infof(ctx, "Retrieving components from datastore") | ||
var list AmpComponentsList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/list/components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
update(validationResult) { | ||
if (validationResult.status === 'FAIL') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use an early exit to avoid nesting
if (validationResult.status !== 'FAIL') {
return
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
} | ||
} | ||
return { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: create the return obj at the beginning of the method
const missingElements = {
components: {},
ampRuntime: false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* missingTags: The keys of this dictionary are those missing AMP components. | ||
* missingBaseScriptTag: True if the AMP engine <script> tag is not present. | ||
*/ | ||
_getMissingElements(validationResult) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: rename s/_get/_parse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
*/ | ||
_insertMissingElements(missing, existing) { | ||
const pos = existing.baseScriptTagEnd || existing.lastTag; | ||
if (pos) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again: early exit if(!pos) return
to avoid nesting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
import CodeMirror from 'codemirror'; | ||
|
||
const MISSING_ENGINE_PARAM = 'amphtml engine v0.js script'; | ||
const AMP_BASE_URL = '"https://cdn.ampproject.org/v0.js"'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How hard would it be to also support auto-adding the runtime for amp ads?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be pretty straightforward. I can give it a go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! But only if it doesn't take too much effort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this.
Thanks @sebastianbenz for the comments! I will work through them! On the
From what I can see in the validator spec for I can open a bug against the validator for this one? I think that's probably the appropriate place to try to fix this? What do you reckon? (I ask as I am very new to this....!) |
Hah! You're right! Please file a bug against the validator. |
Added: ampproject/amphtml#18091 for the amp-carousel issue. |
Hi Sebastian, I think I've made a stab at making the changes. Thank you for the review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great thanks! Few nits.
I've also deployed it, but there seems to be a problem Warning: Unknown AMP component : amp-carousel
: https://amp-by-example-staging.appspot.com/playground/
} | ||
|
||
getComponents() { | ||
return this.componentsMap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this return a promise?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sorry, this was rubbish.
this.win.requestIdleCallback(() => this._fetchComponents()); | ||
} | ||
|
||
getComponents() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: rename to simply get as the class has only one functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
playground/serve.go
Outdated
const ( | ||
COMPONENTS_URL = "https://api.github.com/repos/ampproject/amphtml/git/trees/master?recursive=1" | ||
COMPONENTS_MEMCACHE_KEY = "amp-components-list" | ||
COMPONENTS_UPDATE_FREQ_SECONDS = 86400 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add a comment
COMPONENTS_UPDATE_FREQ_SECONDS = 86400 // one day
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Apologies for that Sebastian, I believe it's fixed now. Thanks for the suggestion, my original approach was bad. |
@@ -40,28 +40,28 @@ class AutoImporter { | |||
|
|||
constructor(componentsProvider, editor) { | |||
this.componentsProvider = componentsProvider; | |||
this.componentsMap = {}; | |||
this.componentsMap = componentsProvider.get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd simply assign the components provider as a field. Otherwise a better approach would be to pass the componentsPromise via the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
this.componentsMap.then((components) => { | ||
this.components = this.components || components; | ||
if (validationResult.status !== 'FAIL') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to the beginning of the method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
this._insertMissingElements(missing, existing); | ||
} | ||
this.componentsMap.then((components) => { | ||
this.components = this.components || components; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer if you'd make the state handling more explicit by passing the components variable to _insertMissingElement instead of assigning it to a field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
.then((r) => r.json()) | ||
.then((data) => { | ||
resolve(data); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the fetch fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about a number of backoff retries, then ultimately if there is no success after n tries, assign the the components map to just be empty? What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it's not a critical feature I think it's fine to simply fail and fallback to an empty map at first. We can add a retry later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the fixes. It still doesn't work when deployed (see my other comment).
I've also noticed another thing we need to change: currently the auto import is triggered as soon as I type: amp-access
. However, I might actually want to include amp-access-laterpay
. Is there a way to avoid this?
playground/serve.go
Outdated
log.Infof(ctx, "Retrieving components from memcache") | ||
var components AmpComponentsList | ||
_, err := memcache.Gob.Get(ctx, COMPONENTS_MEMCACHE_KEY, &components) | ||
if err == memcache.ErrCacheMiss { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I deploy the version, the result is always {}
. It's related to this part as it works when I replace this line with if err != nil {
. BTW this is only a problem when I deploy it to app engine, it works locally.
In general can you make sure to handle the errors correctly:
- can you log all errors:
log.Errorf(ctx, "..... %v", err)
- can you make sure to return a corresponding status code if we could not retrieve the list of components.
Regarding adding components too early, one option might be to inspect the token just before the cursor when a validation error takes place using This would mean auto import wouldn't happen til the tag was closed or a space entered to then add attributes. e.g:
Thoughts? |
This sounds like a perfect solution! |
I've added:
|
Great! Taking a look. Can you also rebase against master (to fix the merge conflict). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Only a few nits, but then we're good to merge!
playground/serve.go
Outdated
return dsComponents, nil | ||
} | ||
} else if err != nil { | ||
return nil, &ComponentsReqError{"Memcache error", http.StatusInternalServerError} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should log this error. However, we should also fallback to loading the components in this case. Suggestion:
if err == nil {
return &components, nil
}
if err == memcache.ErrCacheMiss {
log.Infof(ctx, "Components not in memcache, retrieving from datastore")
} else {
log.Errorf(ctx, "Error when retrieving components from memcache: %v, %v", err.Code, err.Message)
}
dsComponents, err := fetchComponentsFromDataStore(ctx)
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Done
playground/serve.go
Outdated
key := datastore.NewKey(ctx, "AmpComponentsList", "ComponentsListKey", 0, nil) | ||
err := datastore.Get(ctx, key, &components) | ||
if err != nil { | ||
log.Warningf(ctx, "Error retrieving components from datastore") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you also log the error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Should I add some documentation anywhere about use of GitHub API key, and how to set it up in DataStore? Does it fit in the README.md, or is it something that anyone deploying would know already..? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot! Would be great if you could add the datastore info into a readme inside the playground dir.
I've added a readme about adding the token to Datastore. |
Fantastic Thanks! |
Brilliant, @garanj! Thank you so much for making this happen! 👏 |
A start point for the FR from @tomayac #1457.
In summary, this PR adds the feature to automatically insert
<script>
elements for AMP components to the<head>
element, when the user adds code requiring that component.Frontend
The frontend adds a module
auto-importer
which listens for events from the AMP Validator. Where the validation result is a failure, the errors are filtered to determine whether any correspond to missing imports.The resulting missing imports list is cross-referenced with the parsed state of the
<head>
element (as the former can sometimes report missing elements that are actually present), to arrive at a list of elements to insert.The logic for insertion is as follows:
<script>
element already exists, insert any missing<script>
component elements directly beneath it.<head>
element.Currently more complex scenarios seeking to group together existing elements, sort them etc are not dealt with, but the output of parsing the
<head>
element does contain what would be required for this.Backend
The frontend, on initialization, obtains a mapping of AMP component to version number from the backend, using the endpoint
/playground/amp-component-versions
This mapping is formed from the GitHub AMP extensions directory structure (thank you @sebastianbenz).
The mapping returned by the endpoint is stored in Memcache, backed by Datastore. Requests that cause a stale list to be served, trigger an update which is achieved through a named TaskQueue entry.
Demo
An demo can be seen in this video