achilles 1.0.0.rc1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +122 -0
- data/MAINTAINERS.md +53 -0
- data/README.md +199 -11
- data/SECURITY.md +50 -0
- data/app/javascript/achilles/application/application.js +38 -1
- data/app/javascript/achilles/application/dom-mutation-observer/observer.js +11 -1
- data/app/javascript/achilles/application/hooks-manager/turbo.js +28 -7
- data/app/javascript/achilles/components/component_base.js +19 -2
- data/app/javascript/achilles/components/component_parser.js +21 -1
- data/app/javascript/achilles/components/components_registry.js +30 -14
- data/docs/core-js-gaps.md +26 -0
- data/docs/release-checklist.md +2 -1
- data/docs/upgrading-to-1.1.0.md +144 -0
- data/docs/upgrading.md +26 -0
- data/lib/achilles/version.rb +1 -1
- data/lib/achilles.rb +1 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d393e3c7e9d04d6a472ba4e41c727fffca5fa3088f5d064a08a0f9cd4b2bfcf2
|
|
4
|
+
data.tar.gz: 911efcf9ddc72755b3b26b9a8ae3d0a5538d4d33ba2f8476d6c4ae4c8aa54e8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a480a6ca6a9b154cdf723aa53560a1caa2a82f41f569a3e2d79dafbd80e50d1dea7a9c9b18d29f69c8eb9d693af79b358929929b9842a014645eaf939105dea5
|
|
7
|
+
data.tar.gz: 52f50f6c02b169e7d592363571da480961aa6ac52a856aaca72dc3f26f4e5c557e9b5c918e24f18bdfec6d5d37685936e49678e093ea182d4dc8730bad6d898c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Achilles will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## 1.
|
|
5
|
+
## 1.1.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added explicit `Application#start` and `Application#stop` lifecycle methods.
|
|
10
|
+
- Added opt-in `Application#strictLifecycleErrors` handling for tests and
|
|
11
|
+
development.
|
|
12
|
+
- Added browser system coverage for nested components inside Turbo form
|
|
13
|
+
replacement and Turbo Drive navigation.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `Application` no longer starts automatically from the constructor. Applications
|
|
18
|
+
should register component classes, then call `achilles.start()`. See
|
|
19
|
+
[docs/upgrading-to-1.1.0.md](docs/upgrading-to-1.1.0.md).
|
|
20
|
+
- Elements with `data-component-class` must now have a non-empty `id`; invalid
|
|
21
|
+
component roots are skipped with a console error.
|
|
22
|
+
- Component teardown now runs from child components to parent components.
|
|
23
|
+
- Component parentage now follows DOM ancestry under the synthetic `Page` root.
|
|
24
|
+
- Turbo lifecycle hooks are now attached by `start()` and removed by `stop()`.
|
|
25
|
+
- Achilles now requires `turbo-rails` so Turbo importmap assets are available to
|
|
26
|
+
host applications.
|
|
27
|
+
|
|
28
|
+
## 1.0.0
|
|
6
29
|
|
|
7
30
|
### Added
|
|
8
31
|
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Code Of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We want Achilles to be a respectful, useful, and practical open source project.
|
|
6
|
+
|
|
7
|
+
Everyone participating in the project is expected to communicate with clarity,
|
|
8
|
+
patience, and respect. Disagreement is fine. Harassment, personal attacks, and
|
|
9
|
+
abusive behavior are not.
|
|
10
|
+
|
|
11
|
+
## Expected Behavior
|
|
12
|
+
|
|
13
|
+
- Be respectful of differing experience levels and viewpoints.
|
|
14
|
+
- Keep technical criticism focused on the code, design, or documentation.
|
|
15
|
+
- Assume good intent, but be direct when something is unclear or risky.
|
|
16
|
+
- Help keep issues and pull requests actionable.
|
|
17
|
+
- Respect maintainers' time and project scope.
|
|
18
|
+
|
|
19
|
+
## Unacceptable Behavior
|
|
20
|
+
|
|
21
|
+
- Harassment, threats, or personal attacks.
|
|
22
|
+
- Insults, slurs, or discriminatory language.
|
|
23
|
+
- Public or private abuse of contributors or maintainers.
|
|
24
|
+
- Publishing someone's private information without permission.
|
|
25
|
+
- Repeated off-topic disruption after maintainers ask for the discussion to
|
|
26
|
+
stop or move elsewhere.
|
|
27
|
+
|
|
28
|
+
## Enforcement
|
|
29
|
+
|
|
30
|
+
Maintainers may remove comments, close issues, reject pull requests, or block
|
|
31
|
+
participants who violate this code of conduct.
|
|
32
|
+
|
|
33
|
+
To report a concern, email:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
jey@jeygeethan.com
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Reports will be reviewed privately. Please include relevant links, screenshots,
|
|
40
|
+
or context when possible.
|
|
41
|
+
|
|
42
|
+
## Scope
|
|
43
|
+
|
|
44
|
+
This code of conduct applies to Achilles project spaces, including GitHub
|
|
45
|
+
issues, pull requests, discussions, release comments, and private project
|
|
46
|
+
communication with maintainers.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time to improve Achilles.
|
|
4
|
+
|
|
5
|
+
Achilles is intentionally small. Contributions should keep the public API clear,
|
|
6
|
+
the Rails + Turbo integration reliable, and the documentation practical for
|
|
7
|
+
applications that already use the gem.
|
|
8
|
+
|
|
9
|
+
## Local Setup
|
|
10
|
+
|
|
11
|
+
Install dependencies:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run the dummy Rails app:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bin/rails server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
http://localhost:3000/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The dummy app includes a working counter component that exercises the basic
|
|
30
|
+
Achilles flow.
|
|
31
|
+
|
|
32
|
+
## Test Commands
|
|
33
|
+
|
|
34
|
+
Run the Rails and JavaScript lifecycle tests:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bin/rails test
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Run the browser system test:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bin/rails test:system
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run JavaScript syntax checks:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
for file in $(find app/javascript/achilles test/dummy/app/javascript -name '*.js' -print); do node --input-type=module --check < "$file" || exit 1; done
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run the dummy app asset precompile check:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
RAILS_ENV=test bin/rails app:assets:precompile
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run the full local verification set:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bin/rails test
|
|
62
|
+
bin/rails test:system
|
|
63
|
+
for file in $(find app/javascript/achilles test/dummy/app/javascript -name '*.js' -print); do node --input-type=module --check < "$file" || exit 1; done
|
|
64
|
+
RAILS_ENV=test bin/rails app:assets:precompile
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## System Test Browser Requirements
|
|
68
|
+
|
|
69
|
+
System tests use Selenium with headless Chrome.
|
|
70
|
+
|
|
71
|
+
On CI, GitHub's Ubuntu runner provides Chrome. Locally, set these environment
|
|
72
|
+
variables if Chrome or chromedriver are installed in non-standard locations:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
CHROME_BIN=/path/to/chrome CHROMEDRIVER_PATH=/path/to/chromedriver bin/rails test:system
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If Chrome is not available, the system test base skips browser tests cleanly.
|
|
79
|
+
|
|
80
|
+
## Pull Request Guidelines
|
|
81
|
+
|
|
82
|
+
- Keep changes scoped.
|
|
83
|
+
- Add or update tests for behavior changes.
|
|
84
|
+
- Update the README or docs for public API changes.
|
|
85
|
+
- Add or update an upgrade guide for changes existing applications must make.
|
|
86
|
+
- Mention breaking changes clearly in the pull request.
|
|
87
|
+
- Do not introduce a new runtime dependency without explaining why it belongs in
|
|
88
|
+
a small lifecycle gem.
|
|
89
|
+
- Prefer browser DOM APIs in Achilles internals.
|
|
90
|
+
- Keep jQuery usage out of Achilles internals. Applications may still use jQuery
|
|
91
|
+
explicitly in their own components.
|
|
92
|
+
|
|
93
|
+
## Public API Expectations
|
|
94
|
+
|
|
95
|
+
Treat these as public API:
|
|
96
|
+
|
|
97
|
+
- `Application`
|
|
98
|
+
- `ComponentBase`
|
|
99
|
+
- `ComponentsClassMapper`
|
|
100
|
+
- `data-component-class`
|
|
101
|
+
- `setup()`
|
|
102
|
+
- `teardown()`
|
|
103
|
+
- `rootElement()`
|
|
104
|
+
- `rootNode()`
|
|
105
|
+
- `rootElementSelector()`
|
|
106
|
+
|
|
107
|
+
Changes to these APIs need tests, documentation, and changelog notes.
|
|
108
|
+
|
|
109
|
+
## Release Process
|
|
110
|
+
|
|
111
|
+
Use [docs/release-checklist.md](docs/release-checklist.md) for prereleases and
|
|
112
|
+
final releases.
|
|
113
|
+
|
|
114
|
+
Maintainer release ownership and project decision guidelines are documented in
|
|
115
|
+
[MAINTAINERS.md](MAINTAINERS.md).
|
|
116
|
+
|
|
117
|
+
Before releasing:
|
|
118
|
+
|
|
119
|
+
- confirm CI is green
|
|
120
|
+
- run the full local verification set
|
|
121
|
+
- build the gem with `gem build achilles.gemspec`
|
|
122
|
+
- test release candidates in at least one real application
|
data/MAINTAINERS.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Maintainers
|
|
2
|
+
|
|
3
|
+
Achilles is maintained as a small, stable Rails + Turbo lifecycle gem.
|
|
4
|
+
|
|
5
|
+
The maintainer goal is to keep the public API easy to understand, the runtime
|
|
6
|
+
surface small, and releases safe for applications that already depend on the
|
|
7
|
+
gem.
|
|
8
|
+
|
|
9
|
+
## Decision Making
|
|
10
|
+
|
|
11
|
+
Maintainers should prefer changes that:
|
|
12
|
+
|
|
13
|
+
- preserve the small public API
|
|
14
|
+
- improve reliability across Rails, Turbo, and importmap applications
|
|
15
|
+
- reduce hidden runtime dependencies
|
|
16
|
+
- make migration or debugging easier for existing applications
|
|
17
|
+
- include tests for changed behavior
|
|
18
|
+
|
|
19
|
+
Breaking changes are acceptable when they make the project simpler, safer, or
|
|
20
|
+
more predictable. They should be documented in `CHANGELOG.md`, covered by tests,
|
|
21
|
+
and called out in migration notes when existing applications need code changes.
|
|
22
|
+
|
|
23
|
+
## Release Ownership
|
|
24
|
+
|
|
25
|
+
Before publishing a release, a maintainer should:
|
|
26
|
+
|
|
27
|
+
- confirm CI is green
|
|
28
|
+
- run the local verification set in `CONTRIBUTING.md`
|
|
29
|
+
- follow `docs/release-checklist.md`
|
|
30
|
+
- test release candidates in at least one real Rails application when behavior
|
|
31
|
+
changes affect runtime integration
|
|
32
|
+
|
|
33
|
+
Patch releases should be narrow and low risk. Minor releases can add compatible
|
|
34
|
+
APIs or documentation improvements. Major releases may remove deprecated
|
|
35
|
+
behavior or tighten public contracts.
|
|
36
|
+
|
|
37
|
+
## Security Reports
|
|
38
|
+
|
|
39
|
+
Security reports should follow `SECURITY.md`. Reports are handled privately
|
|
40
|
+
until there is a fix or a clear public response.
|
|
41
|
+
|
|
42
|
+
## Community Standards
|
|
43
|
+
|
|
44
|
+
Project participation is covered by `CODE_OF_CONDUCT.md`. Maintainers may close
|
|
45
|
+
issues, reject pull requests, edit discussions, or block participants when
|
|
46
|
+
needed to keep the project focused and respectful.
|
|
47
|
+
|
|
48
|
+
## Adding Maintainers
|
|
49
|
+
|
|
50
|
+
New maintainers should already have a history of useful issues, documentation,
|
|
51
|
+
or code contributions. They should understand the project scope, support the
|
|
52
|
+
public API expectations in `CONTRIBUTING.md`, and be comfortable reviewing
|
|
53
|
+
changes conservatively.
|
data/README.md
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
# Achilles
|
|
2
2
|
|
|
3
3
|
Achilles is a small JavaScript lifecycle layer for Rails + Turbo applications.
|
|
4
|
-
It is
|
|
5
|
-
|
|
4
|
+
It is an explicit component-class alternative to Stimulus for teams that prefer
|
|
5
|
+
plain JavaScript classes mapped directly to DOM nodes.
|
|
6
6
|
|
|
7
7
|
Achilles scans the page for elements with `data-component-class`, instantiates
|
|
8
8
|
the matching JavaScript class, and calls `setup` and `teardown` as Turbo renders
|
|
9
|
-
new pages.
|
|
9
|
+
new pages or new component markup is inserted.
|
|
10
|
+
|
|
11
|
+
## Why Achilles?
|
|
12
|
+
|
|
13
|
+
Rails and Turbo make server-rendered interfaces productive, but many apps still
|
|
14
|
+
need page-specific JavaScript for menus, forms, filters, widgets, charts, and
|
|
15
|
+
small interaction islands.
|
|
16
|
+
|
|
17
|
+
Achilles keeps that JavaScript close to the DOM without requiring controller
|
|
18
|
+
naming conventions or a build step. You register classes explicitly, mark the
|
|
19
|
+
HTML root node, and implement lifecycle methods.
|
|
20
|
+
|
|
21
|
+
Use Achilles when you want:
|
|
22
|
+
|
|
23
|
+
- explicit JavaScript classes instead of controller naming conventions
|
|
24
|
+
- lifecycle hooks that match Turbo navigation
|
|
25
|
+
- no internal jQuery dependency
|
|
26
|
+
- no JavaScript build step
|
|
27
|
+
- predictable setup and teardown for server-rendered UI
|
|
28
|
+
- simple dynamic DOM registration through `MutationObserver`
|
|
29
|
+
|
|
30
|
+
## Achilles vs Stimulus
|
|
31
|
+
|
|
32
|
+
Stimulus is a strong default for many Rails apps. Achilles is intentionally
|
|
33
|
+
smaller and more explicit.
|
|
34
|
+
|
|
35
|
+
| Concern | Achilles | Stimulus |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Class registration | Explicit mapper registration | File and identifier conventions |
|
|
38
|
+
| DOM marker | `data-component-class` | `data-controller` |
|
|
39
|
+
| Root element API | `this.rootElement()` | `this.element` |
|
|
40
|
+
| Lifecycle | `setup()` and `teardown()` | `connect()` and `disconnect()` |
|
|
41
|
+
| Targets/actions | Use standard DOM APIs | Built-in targets/actions |
|
|
42
|
+
| Dependency | Rails, Turbo, Importmap | Hotwire Stimulus |
|
|
43
|
+
|
|
44
|
+
Choose Achilles if you want a tiny lifecycle layer with explicit class mapping.
|
|
45
|
+
Choose Stimulus if you want its controller ecosystem, targets, values, actions,
|
|
46
|
+
and conventions.
|
|
10
47
|
|
|
11
48
|
## Requirements
|
|
12
49
|
|
|
@@ -39,6 +76,7 @@ import { CounterComponent } from "components/counter_component";
|
|
|
39
76
|
|
|
40
77
|
const achilles = new Application();
|
|
41
78
|
achilles.componentsClassMapper.addComponentClass("CounterComponent", CounterComponent);
|
|
79
|
+
achilles.start();
|
|
42
80
|
```
|
|
43
81
|
|
|
44
82
|
Create components by extending `ComponentBase`:
|
|
@@ -74,13 +112,114 @@ Every component root must have a unique `id`. Achilles uses that id to register
|
|
|
74
112
|
the component, find its root element, and avoid running setup twice for the same
|
|
75
113
|
DOM node.
|
|
76
114
|
|
|
115
|
+
## Dynamic Components
|
|
116
|
+
|
|
117
|
+
Achilles watches the document for inserted component markup. If Turbo Streams,
|
|
118
|
+
custom JavaScript, or another UI flow adds a matching element, Achilles parses
|
|
119
|
+
and sets it up automatically.
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<div id="notification-42" data-component-class="NotificationComponent">
|
|
123
|
+
Saved successfully
|
|
124
|
+
</div>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import { ComponentBase } from "achilles/components/component_base";
|
|
129
|
+
|
|
130
|
+
class NotificationComponent extends ComponentBase {
|
|
131
|
+
setup() {
|
|
132
|
+
this.timeout = window.setTimeout(() => {
|
|
133
|
+
this.rootElement().remove();
|
|
134
|
+
}, 3000);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
teardown() {
|
|
138
|
+
window.clearTimeout(this.timeout);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { NotificationComponent };
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Register the component class once:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
achilles.componentsClassMapper.addComponentClass("NotificationComponent", NotificationComponent);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Example App
|
|
152
|
+
|
|
153
|
+
The dummy Rails app includes a working counter component. See
|
|
154
|
+
[examples/README.md](examples/README.md) for the files and local run command.
|
|
155
|
+
|
|
77
156
|
## Lifecycle
|
|
78
157
|
|
|
79
|
-
- `setup` runs after `turbo:load` and after new matching DOM nodes are inserted.
|
|
80
|
-
- `teardown` runs before Turbo renders a new page.
|
|
81
|
-
- `
|
|
82
|
-
-
|
|
83
|
-
|
|
158
|
+
- `setup()` runs after `turbo:load` and after new matching DOM nodes are inserted.
|
|
159
|
+
- `teardown()` runs before Turbo renders a new page.
|
|
160
|
+
- `setup()` and `teardown()` are called once per registered component instance.
|
|
161
|
+
- Components that attach listeners, timers, observers, subscriptions, or widgets
|
|
162
|
+
should clean them up in `teardown()`.
|
|
163
|
+
|
|
164
|
+
## API Reference
|
|
165
|
+
|
|
166
|
+
### `Application`
|
|
167
|
+
|
|
168
|
+
The top-level Achilles application object.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { Application } from "achilles/application/application";
|
|
172
|
+
|
|
173
|
+
const achilles = new Application();
|
|
174
|
+
achilles.start();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Useful properties:
|
|
178
|
+
|
|
179
|
+
- `componentsClassMapper`: register component classes by name.
|
|
180
|
+
- `componentRegistry`: inspect or manage registered component instances.
|
|
181
|
+
- `timezone`: access the configured app timezone.
|
|
182
|
+
|
|
183
|
+
Call `start()` after registering component classes. Call `stop()` when an
|
|
184
|
+
application instance should remove its Turbo hooks and stop observing the DOM.
|
|
185
|
+
|
|
186
|
+
### `ComponentsClassMapper`
|
|
187
|
+
|
|
188
|
+
Maps `data-component-class` values to JavaScript classes.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
achilles.componentsClassMapper.addComponentClass("MenuComponent", MenuComponent);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `ComponentBase`
|
|
195
|
+
|
|
196
|
+
Base class for application components.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { ComponentBase } from "achilles/components/component_base";
|
|
200
|
+
|
|
201
|
+
class MenuComponent extends ComponentBase {
|
|
202
|
+
setup() {}
|
|
203
|
+
teardown() {}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Useful methods:
|
|
208
|
+
|
|
209
|
+
- `setup()`: override to initialize behavior.
|
|
210
|
+
- `teardown()`: override to clean up behavior before Turbo renders a new page.
|
|
211
|
+
- `rootElement()`: returns the component root DOM element.
|
|
212
|
+
- `rootNode()`: alias for `rootElement()`.
|
|
213
|
+
- `rootElementSelector()`: returns a CSS selector for the component id.
|
|
214
|
+
|
|
215
|
+
### Component Markup
|
|
216
|
+
|
|
217
|
+
```erb
|
|
218
|
+
<div id="account-menu" data-component-class="MenuComponent"></div>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The `data-component-class` value must match a class registered with
|
|
222
|
+
`componentsClassMapper`.
|
|
84
223
|
|
|
85
224
|
## Timezone
|
|
86
225
|
|
|
@@ -93,18 +232,67 @@ value through `achilles.timezone.timezoneString`.
|
|
|
93
232
|
|
|
94
233
|
If no timezone is present, Achilles falls back to `Etc/UTC`.
|
|
95
234
|
|
|
235
|
+
## Upgrading From 0.1.3
|
|
236
|
+
|
|
237
|
+
Achilles `1.0.0` changes `rootElement()` to return a DOM element. It no longer
|
|
238
|
+
returns a jQuery object when `window.$` is present.
|
|
239
|
+
|
|
240
|
+
Old jQuery-style code:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
this.rootElement().addClass("is-open");
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Use DOM APIs:
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
this.rootElement().classList.add("is-open");
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Or wrap explicitly if the application still uses jQuery:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
$(this.rootElement()).addClass("is-open");
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Applications upgrading Achilles should start with the
|
|
259
|
+
[upgrade guide](docs/upgrading.md). Applications upgrading from `0.1.3` should
|
|
260
|
+
also read the [v1 migration guide](docs/migrating-from-0.1.3-to-v1.md).
|
|
261
|
+
|
|
96
262
|
## Contributing
|
|
97
263
|
|
|
264
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, test commands, pull request
|
|
265
|
+
guidelines, and release notes. Project participation is covered by the
|
|
266
|
+
[code of conduct](CODE_OF_CONDUCT.md). Maintainer responsibilities are described
|
|
267
|
+
in [MAINTAINERS.md](MAINTAINERS.md).
|
|
268
|
+
|
|
98
269
|
Run the test suite with:
|
|
99
270
|
|
|
100
271
|
```bash
|
|
101
272
|
bin/rails test
|
|
102
273
|
```
|
|
103
274
|
|
|
104
|
-
|
|
275
|
+
Run the browser system test with:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
bin/rails test:system
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Run the JavaScript syntax check with:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
for file in $(find app/javascript/achilles -name '*.js' -print); do node --input-type=module --check < "$file" || exit 1; done
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Run the dummy app asset precompile check with:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
RAILS_ENV=test bin/rails app:assets:precompile
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Security
|
|
105
294
|
|
|
106
|
-
|
|
107
|
-
[v1 migration guide](docs/migrating-from-0.1.3-to-v1.md).
|
|
295
|
+
Report security issues privately. See [SECURITY.md](SECURITY.md).
|
|
108
296
|
|
|
109
297
|
## License
|
|
110
298
|
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Only the latest minor release of Achilles `1.x` receives security fixes.
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `1.x` | Yes |
|
|
10
|
+
| `< 1.0` | No |
|
|
11
|
+
|
|
12
|
+
## Reporting A Vulnerability
|
|
13
|
+
|
|
14
|
+
Please do not open a public GitHub issue for security vulnerabilities.
|
|
15
|
+
|
|
16
|
+
Report security issues by email:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
jey@jeygeethan.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Include:
|
|
23
|
+
|
|
24
|
+
- affected Achilles version
|
|
25
|
+
- Rails version
|
|
26
|
+
- Ruby version
|
|
27
|
+
- a description of the vulnerability
|
|
28
|
+
- reproduction steps or proof of concept
|
|
29
|
+
- whether the issue is already public
|
|
30
|
+
|
|
31
|
+
## Response Expectations
|
|
32
|
+
|
|
33
|
+
You should receive an acknowledgement within 7 days.
|
|
34
|
+
|
|
35
|
+
If the issue is confirmed, the fix will be prepared privately when practical and
|
|
36
|
+
released with a changelog entry that gives users enough information to upgrade
|
|
37
|
+
without exposing unnecessary exploit details before a fix is available.
|
|
38
|
+
|
|
39
|
+
## Scope
|
|
40
|
+
|
|
41
|
+
Security reports are most useful when they involve Achilles behavior directly,
|
|
42
|
+
including:
|
|
43
|
+
|
|
44
|
+
- unsafe DOM lifecycle behavior
|
|
45
|
+
- asset/importmap packaging issues
|
|
46
|
+
- Rails engine integration issues
|
|
47
|
+
- behavior that could cause applications to execute unintended JavaScript
|
|
48
|
+
|
|
49
|
+
General application security issues in apps that use Achilles should be reported
|
|
50
|
+
to those applications instead.
|
|
@@ -16,6 +16,7 @@ class Application {
|
|
|
16
16
|
_componentsClassMapper;
|
|
17
17
|
_hooksManager;
|
|
18
18
|
_domMutationObserver;
|
|
19
|
+
_started = false;
|
|
19
20
|
|
|
20
21
|
constructor() {
|
|
21
22
|
// Set the application while creating the object
|
|
@@ -53,12 +54,45 @@ class Application {
|
|
|
53
54
|
return this._page;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
get strictLifecycleErrors() {
|
|
58
|
+
return this.componentRegistry.strictLifecycleErrors;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
// Setters
|
|
57
62
|
set engine(engine) {
|
|
58
63
|
this._engine = engine;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
set strictLifecycleErrors(value) {
|
|
67
|
+
this.componentRegistry.strictLifecycleErrors = value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
start() {
|
|
71
|
+
if (this._started) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this._started = true;
|
|
76
|
+
this._hooksManager.start();
|
|
77
|
+
this.setup();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stop() {
|
|
81
|
+
if (!this._started) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this._started = false;
|
|
86
|
+
this._hooksManager.stop();
|
|
87
|
+
this.teardown();
|
|
88
|
+
this._domMutationObserver.stop();
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
setup() {
|
|
92
|
+
if (!this._started) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
this._domMutationObserver.stop();
|
|
63
97
|
this.parseHtmlAndRegisterComponents();
|
|
64
98
|
this.componentRegistry.callSetupForComponent(AppConstants.PageComponentId);
|
|
@@ -71,7 +105,10 @@ class Application {
|
|
|
71
105
|
this.componentRegistry.callTeardownForComponent(AppConstants.PageComponentId);
|
|
72
106
|
// Remove/deregister all components from page except Page
|
|
73
107
|
this.deregisterAllComponentsExceptPage();
|
|
74
|
-
|
|
108
|
+
|
|
109
|
+
if (this._started) {
|
|
110
|
+
this._domMutationObserver.start();
|
|
111
|
+
}
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
parseHtmlAndRegisterComponents() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
class Observer {
|
|
2
2
|
_mutationObserver;
|
|
3
3
|
_callback;
|
|
4
|
+
_callbackScheduled = false;
|
|
4
5
|
|
|
5
6
|
constructor(callback) {
|
|
6
7
|
this._callback = callback;
|
|
@@ -18,10 +19,19 @@ class Observer {
|
|
|
18
19
|
|
|
19
20
|
stop() {
|
|
20
21
|
this._mutationObserver.disconnect();
|
|
22
|
+
this._callbackScheduled = false;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
domChangedCallback(mutationsList, observer) {
|
|
24
|
-
this.
|
|
26
|
+
if (this._callbackScheduled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this._callbackScheduled = true;
|
|
31
|
+
queueMicrotask(() => {
|
|
32
|
+
this._callbackScheduled = false;
|
|
33
|
+
this._callback();
|
|
34
|
+
});
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -2,25 +2,46 @@ class Turbo {
|
|
|
2
2
|
_application;
|
|
3
3
|
_setupCallback;
|
|
4
4
|
_teardownCallback;
|
|
5
|
+
_setupHandler;
|
|
6
|
+
_teardownHandler;
|
|
7
|
+
_started = false;
|
|
5
8
|
|
|
6
9
|
constructor(application, setupCallback, teardownCallback) {
|
|
7
10
|
this._application = application;
|
|
8
11
|
this._setupCallback = setupCallback;
|
|
9
12
|
this._teardownCallback = teardownCallback;
|
|
10
|
-
|
|
11
|
-
this.setupEvents();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
// Setups relevant hooks to the page for component lifecycles. This depends on the framework being used.
|
|
15
16
|
// Here we are using turbo drive, so hooking into that.
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
start() {
|
|
18
|
+
if (this._started) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this._setupHandler = () => {
|
|
18
23
|
this._setupCallback();
|
|
19
|
-
}
|
|
24
|
+
};
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
this._teardownHandler = () => {
|
|
22
27
|
this._teardownCallback();
|
|
23
|
-
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
document.addEventListener("turbo:load", this._setupHandler);
|
|
31
|
+
document.addEventListener("turbo:before-render", this._teardownHandler);
|
|
32
|
+
this._started = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
stop() {
|
|
36
|
+
if (!this._started) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.removeEventListener("turbo:load", this._setupHandler);
|
|
41
|
+
document.removeEventListener("turbo:before-render", this._teardownHandler);
|
|
42
|
+
this._setupHandler = null;
|
|
43
|
+
this._teardownHandler = null;
|
|
44
|
+
this._started = false;
|
|
24
45
|
}
|
|
25
46
|
}
|
|
26
47
|
|
|
@@ -2,8 +2,7 @@ class ComponentBase {
|
|
|
2
2
|
parentComponentId;
|
|
3
3
|
id;
|
|
4
4
|
defaultParams;
|
|
5
|
-
|
|
6
|
-
teardownExecuted = false;
|
|
5
|
+
mounted = false;
|
|
7
6
|
|
|
8
7
|
constructor(id, parentComponentId = 'Page', defaultParams = []) {
|
|
9
8
|
this.id = id;
|
|
@@ -17,6 +16,24 @@ class ComponentBase {
|
|
|
17
16
|
setup() {}
|
|
18
17
|
teardown() {}
|
|
19
18
|
|
|
19
|
+
get setupExecuted() {
|
|
20
|
+
return this.mounted;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set setupExecuted(value) {
|
|
24
|
+
this.mounted = value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get teardownExecuted() {
|
|
28
|
+
return !this.mounted;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set teardownExecuted(value) {
|
|
32
|
+
if(value === true) {
|
|
33
|
+
this.mounted = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
rootElement() {
|
|
21
38
|
return this.rootNode();
|
|
22
39
|
}
|
|
@@ -11,6 +11,10 @@ class ComponentParser {
|
|
|
11
11
|
[...document.querySelectorAll('[data-component-class]')].forEach((elem) => {
|
|
12
12
|
let klassName = elem.dataset.componentClass;
|
|
13
13
|
if(klassName.trim() === '') { return; }
|
|
14
|
+
if(!this.hasValidId(elem)) {
|
|
15
|
+
console.error(`Component root element is missing an id. className: ${klassName} | Element:`, elem);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
let klass = this._componentsClassMapper.getComponentClass(klassName);
|
|
16
20
|
if(typeof klass === 'undefined' || klass === null) {
|
|
@@ -22,7 +26,7 @@ class ComponentParser {
|
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
24
28
|
try {
|
|
25
|
-
let obj = new klass(elem.id,
|
|
29
|
+
let obj = new klass(elem.id, this.parentComponentIdFor(elem))
|
|
26
30
|
this._componentRegistry.registerComponentByObj(obj);
|
|
27
31
|
} catch (e) {
|
|
28
32
|
console.error(`Error parsing component. className: ${klassName} | Element ID: ${elem.id}`);
|
|
@@ -31,6 +35,22 @@ class ComponentParser {
|
|
|
31
35
|
}
|
|
32
36
|
})
|
|
33
37
|
}
|
|
38
|
+
|
|
39
|
+
hasValidId(elem) {
|
|
40
|
+
return typeof elem.id === 'string' && elem.id.trim() !== '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
parentComponentIdFor(elem) {
|
|
44
|
+
let parent = elem.parentElement;
|
|
45
|
+
while(parent) {
|
|
46
|
+
if(parent.dataset?.componentClass && this.hasValidId(parent)) {
|
|
47
|
+
return parent.id;
|
|
48
|
+
}
|
|
49
|
+
parent = parent.parentElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return AppConstants.PageComponentId;
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
export { ComponentParser };
|
|
@@ -3,6 +3,7 @@ import { AppConstants } from "achilles/application/app_constants";
|
|
|
3
3
|
// Contains all the registered components in a page at the current moment
|
|
4
4
|
class ComponentsRegistry {
|
|
5
5
|
_registeredComponents = {};
|
|
6
|
+
strictLifecycleErrors = false;
|
|
6
7
|
|
|
7
8
|
matchingElementsForId(id) {
|
|
8
9
|
return [...document.querySelectorAll('[id]')].filter((elem) => elem.id === id);
|
|
@@ -25,6 +26,14 @@ class ComponentsRegistry {
|
|
|
25
26
|
// Component is already registered. So have to call deregister and teardown
|
|
26
27
|
this.teardownAndDeregister(id);
|
|
27
28
|
}
|
|
29
|
+
let parentComponent = null;
|
|
30
|
+
if(parentComponentId != null) {
|
|
31
|
+
parentComponent = this.getRegisteredComponent(parentComponentId);
|
|
32
|
+
if(!parentComponent) {
|
|
33
|
+
console.error(`Parent component not found while registering component. id: ${id} | parentComponentId: ${parentComponentId}. Skipping registering component`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
|
|
29
38
|
this._registeredComponents[id] = {
|
|
30
39
|
id: id,
|
|
@@ -34,7 +43,6 @@ class ComponentsRegistry {
|
|
|
34
43
|
subComponents: []
|
|
35
44
|
};
|
|
36
45
|
if(parentComponentId != null) {
|
|
37
|
-
let parentComponent = this.getRegisteredComponent(parentComponentId);
|
|
38
46
|
parentComponent.subComponents.push(id);
|
|
39
47
|
}
|
|
40
48
|
this.elementForId(id)?.setAttribute('data-component-registered', 'true');
|
|
@@ -51,7 +59,7 @@ class ComponentsRegistry {
|
|
|
51
59
|
parentComponent.subComponents = parentComponent.subComponents.filter(item => item !== id);
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
this._registeredComponents[id]
|
|
62
|
+
delete this._registeredComponents[id];
|
|
55
63
|
this.elementForId(id)?.removeAttribute('data-component-registered');
|
|
56
64
|
}
|
|
57
65
|
|
|
@@ -68,13 +76,13 @@ class ComponentsRegistry {
|
|
|
68
76
|
return;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
// Call the objs default setup if
|
|
72
|
-
if(component.obj.setup && component.obj.
|
|
79
|
+
// Call the objs default setup if it is not mounted already, then continue to children
|
|
80
|
+
if(component.obj.setup && component.obj.mounted === false) {
|
|
73
81
|
try{
|
|
74
82
|
component.obj.setup(...component.defaultParams);
|
|
75
|
-
component.obj.
|
|
83
|
+
component.obj.mounted = true;
|
|
76
84
|
} catch(e) {
|
|
77
|
-
|
|
85
|
+
this.handleLifecycleError(e);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -89,20 +97,28 @@ class ComponentsRegistry {
|
|
|
89
97
|
if(!component || !component.obj)
|
|
90
98
|
return;
|
|
91
99
|
|
|
92
|
-
// Call
|
|
93
|
-
|
|
100
|
+
// Call teardown for all sub view_components before their parent
|
|
101
|
+
component.subComponents.forEach((subComponentId) => {
|
|
102
|
+
this.callTeardownForComponent(subComponentId);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Call the objs default teardown if it is currently mounted
|
|
106
|
+
if(component.obj.teardown && component.obj.mounted === true) {
|
|
94
107
|
try{
|
|
95
108
|
component.obj.teardown(...component.defaultParams);
|
|
96
|
-
component.obj.
|
|
109
|
+
component.obj.mounted = false;
|
|
97
110
|
} catch(e) {
|
|
98
|
-
|
|
111
|
+
this.handleLifecycleError(e);
|
|
99
112
|
}
|
|
100
113
|
}
|
|
114
|
+
}
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
handleLifecycleError(error) {
|
|
117
|
+
console.error(error);
|
|
118
|
+
|
|
119
|
+
if(this.strictLifecycleErrors) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
teardownAndDeregister(id) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Core JavaScript Gaps
|
|
2
|
+
|
|
3
|
+
This is the current improvement backlog for Achilles core JavaScript. API
|
|
4
|
+
reference docs should wait until the structure settles.
|
|
5
|
+
|
|
6
|
+
## Completed
|
|
7
|
+
|
|
8
|
+
- Added an explicit `Application#start` and `Application#stop` lifecycle.
|
|
9
|
+
- Made Turbo event listeners removable.
|
|
10
|
+
- Validated that component roots have non-empty ids before registration.
|
|
11
|
+
- Changed teardown order so child components tear down before parent components.
|
|
12
|
+
- Deleted deregistered registry entries instead of leaving `null` tombstones.
|
|
13
|
+
- Batched mutation observer setup work with a microtask debounce.
|
|
14
|
+
- Replaced one-way lifecycle flags with a `mounted` state while keeping
|
|
15
|
+
`setupExecuted` and `teardownExecuted` as compatibility aliases.
|
|
16
|
+
- Added tests for missing ids, duplicate starts, listener cleanup,
|
|
17
|
+
child-before-parent teardown, dynamic insertion batching, deregistration, and
|
|
18
|
+
remounting a reused component instance.
|
|
19
|
+
- Chose a DOM ancestry model for nested components under the single synthetic
|
|
20
|
+
`Page` root.
|
|
21
|
+
- Split JavaScript tests by parser, registry, application, Turbo hooks, and
|
|
22
|
+
component base responsibility.
|
|
23
|
+
- Added package/file-list regression coverage for the gemspec.
|
|
24
|
+
- Added opt-in strict lifecycle error handling for tests and development.
|
|
25
|
+
|
|
26
|
+
## Remaining Priority Gaps
|
data/docs/release-checklist.md
CHANGED
|
@@ -11,7 +11,8 @@ Use this checklist for `1.0.0.rc1` and future releases.
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
bin/rails test
|
|
14
|
-
|
|
14
|
+
bin/rails test:system
|
|
15
|
+
for file in $(find app/javascript/achilles test/dummy/app/javascript -name '*.js' -print); do node --input-type=module --check < "$file" || exit 1; done
|
|
15
16
|
RAILS_ENV=test bin/rails app:assets:precompile
|
|
16
17
|
```
|
|
17
18
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Upgrading To 1.1.0
|
|
2
|
+
|
|
3
|
+
This guide covers application-facing changes in Achilles `1.1.0`.
|
|
4
|
+
|
|
5
|
+
## Application Startup
|
|
6
|
+
|
|
7
|
+
Achilles applications must now be started explicitly.
|
|
8
|
+
|
|
9
|
+
Before:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Application } from "achilles/application/application";
|
|
13
|
+
import { MenuComponent } from "components/menu_component";
|
|
14
|
+
|
|
15
|
+
const achilles = new Application();
|
|
16
|
+
achilles.componentsClassMapper.addComponentClass("MenuComponent", MenuComponent);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
After:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { Application } from "achilles/application/application";
|
|
23
|
+
import { MenuComponent } from "components/menu_component";
|
|
24
|
+
|
|
25
|
+
const achilles = new Application();
|
|
26
|
+
achilles.componentsClassMapper.addComponentClass("MenuComponent", MenuComponent);
|
|
27
|
+
achilles.start();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Call `start()` after registering component classes. This lets applications
|
|
31
|
+
control when Achilles parses the DOM and attaches Turbo lifecycle hooks.
|
|
32
|
+
|
|
33
|
+
## Stopping An Application
|
|
34
|
+
|
|
35
|
+
Applications that create temporary Achilles instances can now call:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
achilles.stop();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`stop()` removes Turbo lifecycle hooks and stops observing DOM mutations.
|
|
42
|
+
|
|
43
|
+
Most Rails applications only need one long-lived Achilles instance and do not
|
|
44
|
+
need to call `stop()` manually.
|
|
45
|
+
|
|
46
|
+
## Search Checklist
|
|
47
|
+
|
|
48
|
+
In each application, search for Achilles construction:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rg "new Application"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For every application instance, confirm component classes are registered before
|
|
55
|
+
calling `start()`.
|
|
56
|
+
|
|
57
|
+
## Component Root Ids
|
|
58
|
+
|
|
59
|
+
Every element with `data-component-class` must have a non-empty `id`.
|
|
60
|
+
|
|
61
|
+
Before:
|
|
62
|
+
|
|
63
|
+
```erb
|
|
64
|
+
<div data-component-class="MenuComponent"></div>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After:
|
|
68
|
+
|
|
69
|
+
```erb
|
|
70
|
+
<div id="menu" data-component-class="MenuComponent"></div>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Achilles uses component root ids as registry keys. Components without ids are
|
|
74
|
+
skipped and reported in the browser console.
|
|
75
|
+
|
|
76
|
+
Search for component roots without ids:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
rg "data-component-class"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Nested Components
|
|
83
|
+
|
|
84
|
+
Achilles now builds the component tree from DOM ancestry.
|
|
85
|
+
|
|
86
|
+
- `Page` remains the single synthetic root component.
|
|
87
|
+
- A component's parent is its nearest ancestor element with
|
|
88
|
+
`data-component-class`.
|
|
89
|
+
- If no component ancestor exists, the parent is `Page`.
|
|
90
|
+
|
|
91
|
+
For example:
|
|
92
|
+
|
|
93
|
+
```erb
|
|
94
|
+
<div id="dashboard" data-component-class="DashboardComponent">
|
|
95
|
+
<div id="filters" data-component-class="FiltersComponent"></div>
|
|
96
|
+
</div>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The component tree is:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
Page
|
|
103
|
+
dashboard
|
|
104
|
+
filters
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Teardown Order
|
|
108
|
+
|
|
109
|
+
Component teardown now runs from children to parents.
|
|
110
|
+
|
|
111
|
+
Before this change, a parent component's `teardown()` could run before its child
|
|
112
|
+
components. Now child components clean up first, and the parent cleans up after
|
|
113
|
+
its subtree.
|
|
114
|
+
|
|
115
|
+
Review parent components whose `teardown()` removes DOM nodes, shared event
|
|
116
|
+
targets, third-party widgets, or state that child components also use during
|
|
117
|
+
cleanup.
|
|
118
|
+
|
|
119
|
+
## Strict Lifecycle Errors
|
|
120
|
+
|
|
121
|
+
By default, Achilles logs `setup()` and `teardown()` errors and keeps the page
|
|
122
|
+
running.
|
|
123
|
+
|
|
124
|
+
Tests and development environments can opt into strict lifecycle errors:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const achilles = new Application();
|
|
128
|
+
achilles.strictLifecycleErrors = true;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When strict mode is enabled, `setup()` and `teardown()` errors are still logged
|
|
132
|
+
and then re-raised.
|
|
133
|
+
|
|
134
|
+
## Manual Test Checklist
|
|
135
|
+
|
|
136
|
+
After updating an application:
|
|
137
|
+
|
|
138
|
+
- boot the app
|
|
139
|
+
- open a page with Achilles components
|
|
140
|
+
- confirm component `setup()` methods run
|
|
141
|
+
- navigate with Turbo to another page and back
|
|
142
|
+
- confirm `teardown()` still runs before Turbo renders a new page
|
|
143
|
+
- insert any dynamic component markup the app supports
|
|
144
|
+
- check the browser console for Achilles errors
|
data/docs/upgrading.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrading Achilles
|
|
2
|
+
|
|
3
|
+
Use this page as the entry point for application upgrades.
|
|
4
|
+
|
|
5
|
+
Achilles should have an upgrade note for every release that changes application
|
|
6
|
+
setup, component markup, lifecycle behavior, or public APIs.
|
|
7
|
+
|
|
8
|
+
## Current Upgrade Notes
|
|
9
|
+
|
|
10
|
+
- [Upgrading to 1.1.0](upgrading-to-1.1.0.md)
|
|
11
|
+
- [Migrating from 0.1.3 to v1](migrating-from-0.1.3-to-v1.md)
|
|
12
|
+
|
|
13
|
+
## Upgrade Policy
|
|
14
|
+
|
|
15
|
+
Before upgrading an existing application:
|
|
16
|
+
|
|
17
|
+
1. Read the guide for the target version.
|
|
18
|
+
2. Upgrade one application first.
|
|
19
|
+
3. Run the application's test suite.
|
|
20
|
+
4. Visit pages with Achilles components.
|
|
21
|
+
5. Verify Turbo navigation and dynamically inserted components.
|
|
22
|
+
6. Check the browser console for lifecycle, missing component class, or duplicate
|
|
23
|
+
id errors.
|
|
24
|
+
|
|
25
|
+
For breaking changes, prefer testing a release candidate in a real application
|
|
26
|
+
before upgrading every project.
|
data/lib/achilles/version.rb
CHANGED
data/lib/achilles.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: achilles
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jey Geethan
|
|
@@ -60,9 +60,13 @@ extensions: []
|
|
|
60
60
|
extra_rdoc_files: []
|
|
61
61
|
files:
|
|
62
62
|
- CHANGELOG.md
|
|
63
|
+
- CODE_OF_CONDUCT.md
|
|
64
|
+
- CONTRIBUTING.md
|
|
65
|
+
- MAINTAINERS.md
|
|
63
66
|
- MIT-LICENSE
|
|
64
67
|
- README.md
|
|
65
68
|
- Rakefile
|
|
69
|
+
- SECURITY.md
|
|
66
70
|
- app/assets/config/achilles/manifest.js
|
|
67
71
|
- app/assets/stylesheets/achilles/application.css
|
|
68
72
|
- app/controllers/achilles/application_controller.rb
|
|
@@ -83,8 +87,11 @@ files:
|
|
|
83
87
|
- app/views/layouts/achilles/application.html.erb
|
|
84
88
|
- config/importmap.rb
|
|
85
89
|
- config/routes.rb
|
|
90
|
+
- docs/core-js-gaps.md
|
|
86
91
|
- docs/migrating-from-0.1.3-to-v1.md
|
|
87
92
|
- docs/release-checklist.md
|
|
93
|
+
- docs/upgrading-to-1.1.0.md
|
|
94
|
+
- docs/upgrading.md
|
|
88
95
|
- docs/v1-roadmap.md
|
|
89
96
|
- lib/achilles.rb
|
|
90
97
|
- lib/achilles/engine.rb
|