-
Notifications
You must be signed in to change notification settings - Fork 8
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
Tram-One, now with Types! Typescript! v11.0.0 #175
Conversation
@@ -31,7 +31,7 @@ jobs: | |||
fingerprints: | |||
- "64:3e:d4:d8:4b:95:68:79:d3:3b:ab:b1:5c:fa:2d:3d" | |||
|
|||
- run: npm install | |||
- run: npm ci |
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 project originally predated npm ci
, but now that it exists, we should be using that, and not regenerating the lock file on every PR.
# SSL certificates | ||
.ssl | ||
|
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.
This was most certainly an artifact of when we had the website hosted from this repo, since that is no longer a thing, we can remove this
.prettierrc.js
Outdated
module.exports = { | ||
useTabs: true, | ||
singleQuote: true, | ||
printWidth: 120, | ||
}; |
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.
Using prettier instead of xo.
The prettier config is more known to work with typescript, and xo was breaking too much.
Eventually I'd like to rip out the eslint config for tram-one projects as it is today. It really should be something that validates tram-one things (e.g., I would love to lint template spacing, and hook placement). Right now, it's just silly opinions, and that doesn't really help anyone.
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.
This includes using semicolons (which sadly blows the number of lines changed for the project super high). ASI has burned me enough times (read, more than 0 times), and I think with the new Typescript syntax, it's easier if we are on the same standard as everyone else.
it('should process effects on the root node', async () => { | ||
// start the app | ||
const { container } = startApp(); | ||
|
||
// verify that it (eventually) has loaded: true | ||
await waitFor(() => { | ||
expect(getByText(container, 'Root Loaded: true')).toBeVisible(); | ||
}); | ||
}); |
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.
New test, we didn't really have something to validate this, but we did have code in the project that would have caused this to fail and nothing else.
* component to test url parameters | ||
*/ | ||
export default () => { | ||
const mirrorable = useGlobalStore('mirrorable-input') as InputObject; |
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.
The type isn't actually required if you pass in a default value. The fact that this option exists is cool, and possible because of function overloading.
it('should warn if a component does not return anything', () => { | ||
expect(() => startBrokenApp('empty')()).toThrowError('Tram-One: expected component to return an Element, instead got undefined. Verify the component is a function that returns DOM.') | ||
}) | ||
|
||
it('should warn if a component does not return an element', () => { | ||
expect(() => startBrokenApp('non-dom')()).toThrowError('Tram-One: expected component to return an Element, instead got string. Verify the component is a function that returns DOM.') | ||
}) | ||
|
||
it('should warn if a component returns an array', () => { | ||
expect(() => startBrokenApp('array')()).toThrowError('Tram-One: Sorry, Tram-One does not currently support array returns. Wrap components in an element before returning.') | ||
}) |
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.
aforementioned removed tests
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.
the other ones in this file still exist because they are more runtime dependent. Technically we should be able to catch the "warn if a hook is called outside of a component context", but that requires building lint or typescript rules around that. Nothing obvious or trivial was available online on how to do this.
package.json
Outdated
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "tram-one", | |||
"version": "10.1.11", | |||
"version": "11.0.0", |
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.
Major bump. In reality, this might not be breaking for JS projects, but anyone using Typescript would start getting compile time errors / warnings, so for that reason, it should be breaking.
src/build-mounting-container.ts
Outdated
/** | ||
* Function to determine (or create) the element that we will mount our tram-one app onto | ||
* @param target either a CSS selector, or HTMLElement to attach the component to. | ||
* This elememnt should be initially empty. | ||
* | ||
* @returns the container, now with a div that tram-one can manage | ||
*/ | ||
export default (target: ElementOrSelector): HTMLElement => { |
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.
DocStrings no longer have types in them. These should be inherited from the Types in the method definition / implementation.
src/dom.ts
Outdated
const hookedTagFunction = (props: Props, children: Children) => { | ||
// push a new branch onto the working key so any values that need to be unique among components | ||
// but consistent across renders can be read | ||
const stringifiedProps = JSON.stringify(props); | ||
const newBranch = `${tagName}[${stringifiedProps}]`; | ||
pushWorkingKeyBranch(TRAM_HOOK_KEY, newBranch); | ||
|
||
// increment branch so that we have a unique value (in case we are rendering a list of components) | ||
incrementWorkingKeyBranch(TRAM_HOOK_KEY); | ||
const uniqueBranch = copyWorkingKey(TRAM_HOOK_KEY); | ||
|
||
// create a tag function that has the args passed in | ||
const populatedTagFunction = () => { | ||
// reset working key so we have the correct place when starting a new component | ||
restoreWorkingKey(TRAM_HOOK_KEY, uniqueBranch); | ||
|
||
return tagFunction(props, children); | ||
}; |
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.
Previously this function used (...args)
instead of (props, children)
in method definitions. Potentially at the time I didn't know what this method definition would be, or wanted it to be more flexible, or maybe it was really really early on when component methods had a ton of things in them. Either way, it looks like this now, and should be easier to understand and more explicit.
@@ -1,33 +1,27 @@ | |||
/** | |||
/* |
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.
In some places I had inadvertently (or lazily) use /**
which makes blocks more readily, but gets consumed as docstrings for the next variable, which was not the intention.
// this sadly needs to be wrapped in some element so we can process effects | ||
// otherwise the root node will not have effects applied on it | ||
const renderedApp = html`<div><app /></div>`; |
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.
This is the code that was previously untested. Ideally this wouldn't exist, and potentially we should annotate it / change it to be more clearly intended for tram-one (with a different tag, or an id). However, any more additions to this would push us away from just getting rid of it altogether.
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 look to change this into a Document Fragment 🤔
Never mind, probably runs into the same issues.
src/mutation-observer.ts
Outdated
const nodeFilterForTramOneComponent = { acceptNode: isTramOneComponent }; | ||
const componentWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, nodeFilterForTramOneComponent); |
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.
Funny story, previously, this was implemented wrong. createTreeWalker takes in an object with a key acceptNode
. Typescript caught this, I'm not sure how this was working before, and sadly it doesn't seem to have improved performance. Either way, it's correct now 👍
window['tram-space'] = {}; | ||
}; | ||
|
||
export const buildNamespace = <NamespaceStore>(constructor: () => NamespaceStore) => { |
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.
In order to have the get
set
and setup
functions all produce the same Type, we now have one generic function whose type is inferred by the constructor that is used.
* | ||
* It has a similar interface to React's useState | ||
*/ | ||
export default <Store extends StoreObject>(key?: string, value?: Store): Store => { |
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.
Previously this function verified the type of the string and value, now that can be verified with Typescript! JS users will still get annotations in Typescript capable editors.
// we'll assume that the element is an HTMLInputElement, in reality other kinds of elements will be caught here, | ||
// but that's fine, since they have null as selection attributes, and setting them to null is fine | ||
const activeElement = document.activeElement as HTMLInputElement; |
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.
Pretty much what it says in the comment. This is because Typescript (and somewhat inaccurately so) complains that "Element" doesn't have the selection attributes (e.g. selectionStart
), in reality, these will be null, which I guess for Typescript is just as bad, but for us, that is fine.
src/process-effects.ts
Outdated
* | ||
* These are later processed by the mutation-observer, and cleaned up when the node is removed by the mutation-observer. | ||
*/ | ||
export default (tagFunction: TramOneComponent) => { |
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.
Previously this function verified that the tagFunction
returned an Element, but now we get that with Typescript!
src/use-global-store.ts
Outdated
/** | ||
* @name useGlobalObservable | ||
* @link https://tram-one.io/#use-global-observable | ||
* @description | ||
* Hook that stores global state and makes it accessible in the entire app. | ||
* | ||
* If the subfield of an object, or element of an array is updated | ||
* it will cause only the components that are dependent on that value to update. | ||
* | ||
* @param key a unique string to write and read the global value | ||
* @param defaultValue the default value to start the store at | ||
* | ||
* @returns the store to interact with. | ||
*/ | ||
function useGlobalStore<Store extends StoreObject>(key: string, defaultValue: Store): Store; | ||
/** | ||
* @name useGlobalObservable | ||
* @link https://tram-one.io/#use-global-observable | ||
* @description | ||
* Hook that stores global state and makes it accessible in the entire app. | ||
* | ||
* If the subfield of an object, or element of an array is updated | ||
* it will cause only the components that are dependent on that value to update. | ||
* | ||
* @param key a unique string to write and read the global value | ||
* | ||
* @returns the store to interact with. | ||
*/ | ||
function useGlobalStore(key: string): unknown; | ||
/** Implementation of the two function definitions */ | ||
function useGlobalStore(key: string, defaultValue?): any { |
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.
Function overloading! The first two definitions (19 & 33) are the two possible method interfaces, and the third definition (35) is the more generic version that is used for the code implementation. This allows us to have a version of the method that knows the return type (in this case, inferred from the defaultValue
). In cases where you don't pass a default value, you have to specifically say what the type is.
### Is Tram-One for Javascript or Typescript? | ||
|
||
Both! While the source code and type checking exist in Typescript, smart editors (such as Visual Studio Code), will make use of | ||
the Typescript annotations regardless of what language you work in! | ||
|
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 love to make a more general FAQ and Principles section in the Tram-One Website, but in the interim, this will have to do 😅
When playing around in a typescript project, people can use the following syntax to verify the component follows the correct interface: import { registerHtml, TramOneComponent } from 'tram-one'
const html = registerHtml()
const AppHeader: TramOneComponent = (props, children) => {
return html`
<h1 class="app-header">
${children}
</h1>
`
}
export default AppHeader |
// NOTE: This won't work since Tram-One does not (right now) support server-side rendering | ||
// This is on our backlog though, and if it is something that you would like, please vote / comment here: | ||
// https://github.com/Tram-One/tram-one/issues/181 |
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.
Domino was removed, so this runkit example won't work, but I'm keeping it here because one day it hopefully will 😄
/** | ||
* component to test url parameters | ||
*/ | ||
const account: TramOneComponent = () => { |
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.
This is a pattern components can use, to verify that they follow the interface for a "TramOneComponent"
// file needed for importing package-json | ||
// see https://rollupjs.org/guide/en/#using-untranspiled-config-files | ||
module.exports = require('./package.json'); |
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.
basically what the comments say, this is required for loading the package.json in es modules.
"type": "module", | ||
"exports": { | ||
".": { | ||
"import": "./dist/tram-one.mjs", | ||
"require": "./dist/tram-one.cjs" | ||
} | ||
}, |
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.
using types and export, this may not be totally consumed correctly until the next version of typescript, but we can future proof it 👍
// decorate the properties expected on TramOneElements (see node-names.ts) | ||
tagResult[TRAM_TAG] = true; | ||
// we won't decorate TRAM_TAG_REACTION, that needs to be done later when we observe the tag | ||
tagResult[TRAM_TAG_NEW_EFFECTS] = tagResult[TRAM_TAG_NEW_EFFECTS] || []; | ||
tagResult[TRAM_TAG_CLEANUP_EFFECTS] = tagResult[TRAM_TAG_NEW_EFFECTS] || []; |
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.
decorating all the properties of a tram one tag, so that they are available for sure
src/tram-one.ts
Outdated
export { | ||
ElementOrSelector, | ||
DOMTaggedTemplateFunction, | ||
TramOneComponent, | ||
StoreObject, | ||
CleanupEffect, | ||
Effect, | ||
Props, | ||
Registry, | ||
UrlMatchResults, | ||
} from './types'; |
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.
Exposing these types publicly, so that other Typescript projects can use them directly.
/** | ||
* Type for when we can take a CSS Selector, or an HTML Element (mostly mounting). | ||
*/ | ||
export type ElementOrSelector = [string | HTMLElement][0]; |
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'm not 100% sure about this pattern, but it allows consumers to understand what the library is looking for (rather than just saying "ElementOrSelector")
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.
Awesome stuff! Cannot wait for typescript magic!!
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.
👍
Summary
In an effort to get the advantages of type safety, this PR migrates the source code (and most of the tests) to Typescript.
This gives the project more type safety, and allows our consumers to build projects with type safety (if they are using Typescript). If they aren't using Typescript they still will get proper type annotations when working with compatible editors (such as Visual Studio Code).
Changes
All the changes listed below are detailed in the comments, treat these as a high level breakdown.
Bump Major - v11.0.0
In reality, this might not be breaking for JS projects, but there are a significant number of changes (including the removal of some safety checks for JS and addition of checks for TS). Anyone using Typescript would start getting compile time errors / warnings, so for that reason alone really it should be considered a breaking change.
Main Changes
types.ts
file for typescript type definitionsAuxiliary Changes
...args
for more clear parameter definitionsTesting Changes
Artifacts: typescript...typescript-artifacts
Note about the artifacts
For some reason, CircleCI slowed down between the last master run and this changeset. This was confirmed with a dummy PR which had similar slowdowns (with no changes).Checklist
Post Merge Checklist