ima-claude 2.9.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.
- package/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "ember-discourse"
|
|
3
|
+
description: "Ember/Glimmer component development for Discourse plugins - gjs, apiInitializer, plugin outlets, @tracked, @service, admin UI"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ember for Discourse Plugins
|
|
7
|
+
|
|
8
|
+
Discourse runs Ember Octane with Glimmer components. The plugin extension model is: `apiInitializer` → `renderInOutlet` → `.gjs` component. Everything else is either legacy or an internal Discourse concern.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Adding frontend UI to a Discourse plugin
|
|
13
|
+
- Building admin panel components for a plugin
|
|
14
|
+
- Injecting content into Discourse's UI via plugin outlets
|
|
15
|
+
- Migrating old `decorateWidget` / raw handlebars to modern Glimmer
|
|
16
|
+
|
|
17
|
+
## Core Philosophy
|
|
18
|
+
|
|
19
|
+
> Glimmer components are close to pure functions: `(args, services) → template`. Minimize `@tracked` state. Prefer derived values over stored state.
|
|
20
|
+
|
|
21
|
+
FP lens applied to Ember:
|
|
22
|
+
- **Components** are view functions — args in, DOM out
|
|
23
|
+
- **`@tracked`** is isolated reactive state — use sparingly, only what truly changes
|
|
24
|
+
- **Derived values** via getters — no duplicate state, always in sync
|
|
25
|
+
- **Actions** are the imperative shell — side effects live in methods, not templates
|
|
26
|
+
- **Services** are dependency injection — inject, don't import singletons
|
|
27
|
+
|
|
28
|
+
**Foundation**: Reference `../discourse/SKILL.md` for the Ruby side of plugin development.
|
|
29
|
+
|
|
30
|
+
## The Modern Stack (use these)
|
|
31
|
+
|
|
32
|
+
| What | Modern | Deprecated / Avoid |
|
|
33
|
+
|------|--------|--------------------|
|
|
34
|
+
| File format | `.gjs` (Glimmer JS) | `.hbs` + `.js` pairs, `.hbr`, `.raw.hbs` |
|
|
35
|
+
| Entry point | `apiInitializer` | bare `withPluginApi` initializer export |
|
|
36
|
+
| Inject into UI | `api.renderInOutlet` | `registerConnectorClass`, `decorateWidget` |
|
|
37
|
+
| Component base | `@glimmer/component` | `@ember/component` (classic) |
|
|
38
|
+
| Reactive state | `@tracked` | `Ember.set()`, `this.set()` |
|
|
39
|
+
| Services | `@service` decorator | `Ember.inject.service()` |
|
|
40
|
+
| Event handling | `{{on "click" this.handler}}` | `{{action "handler"}}` |
|
|
41
|
+
|
|
42
|
+
## .gjs: The File Format
|
|
43
|
+
|
|
44
|
+
`.gjs` (Glimmer JS) combines the component class and template in one file. It's the current Discourse standard for new components.
|
|
45
|
+
|
|
46
|
+
```gjs
|
|
47
|
+
// assets/javascripts/discourse/components/my-component.gjs
|
|
48
|
+
import Component from "@glimmer/component";
|
|
49
|
+
import { tracked } from "@glimmer/tracking";
|
|
50
|
+
import { action } from "@ember/object";
|
|
51
|
+
import { service } from "@ember/service";
|
|
52
|
+
import DButton from "discourse/components/d-button";
|
|
53
|
+
|
|
54
|
+
export default class MyComponent extends Component {
|
|
55
|
+
@service currentUser;
|
|
56
|
+
@service siteSettings;
|
|
57
|
+
|
|
58
|
+
@tracked isExpanded = false;
|
|
59
|
+
|
|
60
|
+
// Derived value — getter, not stored state
|
|
61
|
+
get greeting() {
|
|
62
|
+
return this.currentUser
|
|
63
|
+
? `Welcome back, ${this.currentUser.username}!`
|
|
64
|
+
: "Welcome to our community!";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@action
|
|
68
|
+
toggleExpanded() {
|
|
69
|
+
this.isExpanded = !this.isExpanded;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div class="my-component">
|
|
74
|
+
<p>{{this.greeting}}</p>
|
|
75
|
+
|
|
76
|
+
<DButton
|
|
77
|
+
@action={{this.toggleExpanded}}
|
|
78
|
+
@label={{if this.isExpanded "collapse" "expand"}}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
{{#if this.isExpanded}}
|
|
82
|
+
<div class="my-component__body">
|
|
83
|
+
{{yield}}
|
|
84
|
+
</div>
|
|
85
|
+
{{/if}}
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Template-only component (no JS needed)
|
|
92
|
+
|
|
93
|
+
```gjs
|
|
94
|
+
// When a component is purely presentational — no class required
|
|
95
|
+
<template>
|
|
96
|
+
<div class="badge-card">
|
|
97
|
+
<img src={{@badge.image_url}} alt={{@badge.name}} />
|
|
98
|
+
<span>{{@badge.name}}</span>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## apiInitializer: The Plugin Entry Point
|
|
104
|
+
|
|
105
|
+
Every plugin's JS starts here. One initializer file per plugin feature area.
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// assets/javascripts/discourse/initializers/my-plugin.js
|
|
109
|
+
import { apiInitializer } from "discourse/lib/api";
|
|
110
|
+
import MyBanner from "../components/my-banner";
|
|
111
|
+
import MyComponent from "../components/my-component";
|
|
112
|
+
|
|
113
|
+
export default apiInitializer((api) => {
|
|
114
|
+
// Render into a plugin outlet
|
|
115
|
+
api.renderInOutlet("discovery-list-container-top", MyBanner);
|
|
116
|
+
|
|
117
|
+
// shouldRender on the component class controls conditional rendering
|
|
118
|
+
api.renderInOutlet("topic-above-post-stream", MyComponent);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Plugin Outlets: Where to Inject Content
|
|
123
|
+
|
|
124
|
+
Plugin outlets are pre-defined hooks in Discourse's templates. Use `api.renderInOutlet` to inject at them.
|
|
125
|
+
|
|
126
|
+
```gjs
|
|
127
|
+
// assets/javascripts/discourse/components/my-banner.gjs
|
|
128
|
+
import Component from "@glimmer/component";
|
|
129
|
+
import { service } from "@ember/service";
|
|
130
|
+
|
|
131
|
+
export default class MyBanner extends Component {
|
|
132
|
+
@service currentUser;
|
|
133
|
+
|
|
134
|
+
// Static method controls whether this component renders at all.
|
|
135
|
+
// outletArgs contains context from where the outlet sits in the DOM.
|
|
136
|
+
static shouldRender(outletArgs, helper) {
|
|
137
|
+
return helper.siteSettings.my_plugin_enabled && helper.currentUser;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<div class="my-banner">
|
|
142
|
+
Welcome, {{this.currentUser.username}}!
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Post stream outlets (Glimmer post stream — current)
|
|
149
|
+
|
|
150
|
+
```gjs
|
|
151
|
+
import Component from "@glimmer/component";
|
|
152
|
+
import { apiInitializer } from "discourse/lib/api";
|
|
153
|
+
|
|
154
|
+
export default apiInitializer((api) => {
|
|
155
|
+
api.renderAfterWrapperOutlet(
|
|
156
|
+
"post-content-cooked-html",
|
|
157
|
+
class extends Component {
|
|
158
|
+
static shouldRender(args) {
|
|
159
|
+
return args.post.wiki; // args.post is the current post model
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
<template>
|
|
163
|
+
<div class="wiki-notice">This post is a wiki</div>
|
|
164
|
+
</template>
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Finding outlet names
|
|
171
|
+
|
|
172
|
+
Search Discourse core for `<PluginOutlet @name=`:
|
|
173
|
+
```bash
|
|
174
|
+
rg '<PluginOutlet @name=' app/assets/javascripts/discourse/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Common outlets: `discovery-list-container-top`, `topic-above-post-stream`,
|
|
178
|
+
`above-main-container`, `header-icons`, `user-profile-primary`,
|
|
179
|
+
`post-content-cooked-html`, `after-topic-list-area`.
|
|
180
|
+
|
|
181
|
+
## Glimmer Component Patterns
|
|
182
|
+
|
|
183
|
+
### Args vs State
|
|
184
|
+
|
|
185
|
+
```gjs
|
|
186
|
+
import Component from "@glimmer/component";
|
|
187
|
+
import { tracked } from "@glimmer/tracking";
|
|
188
|
+
|
|
189
|
+
export default class UserCard extends Component {
|
|
190
|
+
// @tracked — only for state THIS component owns and mutates
|
|
191
|
+
@tracked showDetails = false;
|
|
192
|
+
|
|
193
|
+
// Derived from args — a getter, never @tracked
|
|
194
|
+
get displayName() {
|
|
195
|
+
return this.args.user.name || this.args.user.username;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get isStaff() {
|
|
199
|
+
return this.args.user.staff;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
<template>
|
|
203
|
+
<div class="user-card {{if this.isStaff 'user-card--staff'}}">
|
|
204
|
+
<h3>{{this.displayName}}</h3>
|
|
205
|
+
{{#if this.showDetails}}
|
|
206
|
+
<p>{{@user.bio_raw}}</p>
|
|
207
|
+
{{/if}}
|
|
208
|
+
</div>
|
|
209
|
+
</template>
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Services — inject, don't import
|
|
214
|
+
|
|
215
|
+
```gjs
|
|
216
|
+
import Component from "@glimmer/component";
|
|
217
|
+
import { service } from "@ember/service";
|
|
218
|
+
|
|
219
|
+
export default class MyFeature extends Component {
|
|
220
|
+
// Common Discourse services
|
|
221
|
+
@service currentUser; // logged-in user (null if anonymous)
|
|
222
|
+
@service siteSettings; // site configuration
|
|
223
|
+
@service router; // programmatic navigation
|
|
224
|
+
@service store; // Ember Data store
|
|
225
|
+
@service session; // session data
|
|
226
|
+
@service modal; // open modals
|
|
227
|
+
@service toasts; // toast notifications (Discourse 3.2+)
|
|
228
|
+
|
|
229
|
+
get canUseFeature() {
|
|
230
|
+
return this.currentUser?.trust_level >= 2
|
|
231
|
+
&& this.siteSettings.my_plugin_enabled;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
<template>
|
|
235
|
+
{{#if this.canUseFeature}}
|
|
236
|
+
...
|
|
237
|
+
{{/if}}
|
|
238
|
+
</template>
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Actions and events
|
|
243
|
+
|
|
244
|
+
```gjs
|
|
245
|
+
import Component from "@glimmer/component";
|
|
246
|
+
import { tracked } from "@glimmer/tracking";
|
|
247
|
+
import { action } from "@ember/object";
|
|
248
|
+
|
|
249
|
+
export default class SearchBox extends Component {
|
|
250
|
+
@tracked query = "";
|
|
251
|
+
@tracked results = [];
|
|
252
|
+
|
|
253
|
+
@action
|
|
254
|
+
updateQuery(event) {
|
|
255
|
+
this.query = event.target.value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@action
|
|
259
|
+
async search() {
|
|
260
|
+
if (!this.query.trim()) return;
|
|
261
|
+
// side effects belong in @action methods, not in getters or templates
|
|
262
|
+
this.results = await this.args.onSearch(this.query);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
<template>
|
|
266
|
+
<input
|
|
267
|
+
type="text"
|
|
268
|
+
value={{this.query}}
|
|
269
|
+
{{on "input" this.updateQuery}}
|
|
270
|
+
/>
|
|
271
|
+
<button type="button" {{on "click" this.search}}>Search</button>
|
|
272
|
+
</template>
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Admin UI Components
|
|
277
|
+
|
|
278
|
+
Admin components live in the `admin/` asset tree and use the same Glimmer patterns.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
assets/javascripts/
|
|
282
|
+
├── discourse/
|
|
283
|
+
│ └── initializers/
|
|
284
|
+
│ └── my-plugin.js # user-facing outlets
|
|
285
|
+
└── admin/
|
|
286
|
+
├── components/
|
|
287
|
+
│ └── my-plugin-admin.gjs # admin panel component
|
|
288
|
+
└── routes/
|
|
289
|
+
└── admin-plugins-my-plugin.js
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```gjs
|
|
293
|
+
// assets/javascripts/admin/components/my-plugin-admin.gjs
|
|
294
|
+
import Component from "@glimmer/component";
|
|
295
|
+
import { tracked } from "@glimmer/tracking";
|
|
296
|
+
import { action } from "@ember/object";
|
|
297
|
+
import { service } from "@ember/service";
|
|
298
|
+
import { ajax } from "discourse/lib/ajax";
|
|
299
|
+
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
300
|
+
import DButton from "discourse/components/d-button";
|
|
301
|
+
import LoadingSpinner from "discourse/components/loading-spinner";
|
|
302
|
+
|
|
303
|
+
export default class MyPluginAdmin extends Component {
|
|
304
|
+
@service currentUser;
|
|
305
|
+
|
|
306
|
+
@tracked stats = null;
|
|
307
|
+
@tracked isLoading = false;
|
|
308
|
+
|
|
309
|
+
@action
|
|
310
|
+
async loadStats() {
|
|
311
|
+
this.isLoading = true;
|
|
312
|
+
try {
|
|
313
|
+
const response = await ajax("/admin/plugins/my-plugin.json");
|
|
314
|
+
this.stats = response.stats;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
popupAjaxError(error);
|
|
317
|
+
} finally {
|
|
318
|
+
this.isLoading = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
<template>
|
|
323
|
+
<div class="my-plugin-admin">
|
|
324
|
+
<h2>My Plugin Admin</h2>
|
|
325
|
+
|
|
326
|
+
{{#if this.isLoading}}
|
|
327
|
+
<LoadingSpinner />
|
|
328
|
+
{{else if this.stats}}
|
|
329
|
+
<p>Total users: {{this.stats.total_users}}</p>
|
|
330
|
+
<p>Pending: {{this.stats.pending}}</p>
|
|
331
|
+
{{/if}}
|
|
332
|
+
|
|
333
|
+
<DButton
|
|
334
|
+
@action={{this.loadStats}}
|
|
335
|
+
@label="my_plugin.admin.load_stats"
|
|
336
|
+
@disabled={{this.isLoading}}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
</template>
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## AJAX: Talking to the Plugin Backend
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
import { ajax } from "discourse/lib/ajax";
|
|
347
|
+
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
348
|
+
|
|
349
|
+
// GET
|
|
350
|
+
const data = await ajax("/my-plugin/endpoint.json");
|
|
351
|
+
|
|
352
|
+
// POST — Discourse's ajax helper automatically includes the CSRF token
|
|
353
|
+
const result = await ajax("/my-plugin/action", {
|
|
354
|
+
type: "POST",
|
|
355
|
+
data: { user_id: userId, value: someValue }
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Always handle errors with popupAjaxError for consistent UX
|
|
359
|
+
try {
|
|
360
|
+
await ajax("/my-plugin/action", { type: "DELETE" });
|
|
361
|
+
} catch (e) {
|
|
362
|
+
popupAjaxError(e);
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Always use `discourse/lib/ajax`, never raw `fetch`.** The helper handles CSRF tokens, error formatting, and Discourse session state automatically.
|
|
367
|
+
|
|
368
|
+
## Security in Ember
|
|
369
|
+
|
|
370
|
+
### Client checks are UX only — backend enforces everything
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
// Hiding/showing UI based on role is fine:
|
|
374
|
+
get showAdminTools() {
|
|
375
|
+
return this.currentUser?.admin;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// But the BACKEND must enforce the same check on every request.
|
|
379
|
+
// Client-side auth is trivially bypassed in browser devtools.
|
|
380
|
+
// There is no such thing as client-side security.
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### No raw HTML injection with user content
|
|
384
|
+
|
|
385
|
+
```handlebars
|
|
386
|
+
{{!-- Safe — Glimmer auto-escapes output --}}
|
|
387
|
+
{{user.bio}}
|
|
388
|
+
|
|
389
|
+
{{!-- Unsafe — raw output, skip escaping only for server-sanitized HTML --}}
|
|
390
|
+
{{! avoid triple-mustache with user-supplied content }}
|
|
391
|
+
|
|
392
|
+
{{!-- For server-sanitized post content, Discourse provides: --}}
|
|
393
|
+
<div>{{html-safe post.cooked}}</div>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Content Security Policy
|
|
397
|
+
|
|
398
|
+
Discourse enforces CSP strictly. These patterns will break or be blocked:
|
|
399
|
+
- Inline `<script>` tags in plugin templates
|
|
400
|
+
- Dynamic code evaluation (`eval`, dynamic code strings)
|
|
401
|
+
- Modifying `innerHTML` directly — use Glimmer templates instead
|
|
402
|
+
- Importing from external CDNs — bundle or use Discourse's asset pipeline
|
|
403
|
+
|
|
404
|
+
## Deprecations to Actively Avoid
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
// DEPRECATED — old connector class pattern (shows deprecation warning)
|
|
408
|
+
api.registerConnectorClass("outlet-name", "connector-name", {
|
|
409
|
+
setupComponent(args, component) { ... }
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// DEPRECATED — widget system (actively being removed in 2025/2026)
|
|
413
|
+
api.decorateWidget("post:after", (helper) => { ... });
|
|
414
|
+
api.createWidget("my-widget", { ... });
|
|
415
|
+
|
|
416
|
+
// DEPRECATED — raw handlebars files (.hbr, .raw.hbs)
|
|
417
|
+
// Breaks with the Glimmer topic list (enabled by default 2025)
|
|
418
|
+
|
|
419
|
+
// DEPRECATED — classic component base class
|
|
420
|
+
import Component from "@ember/component"; // use @glimmer/component instead
|
|
421
|
+
|
|
422
|
+
// DEPRECATED — Ember object mutation helpers
|
|
423
|
+
this.set("myProp", value); // use @tracked + direct assignment: this.myProp = value
|
|
424
|
+
Ember.set(obj, "key", val); // same — direct assignment or @tracked
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## File Naming and Location
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
assets/javascripts/discourse/
|
|
431
|
+
├── initializers/
|
|
432
|
+
│ └── my-plugin.js # apiInitializer — one per feature area
|
|
433
|
+
├── components/
|
|
434
|
+
│ ├── my-feature.gjs # kebab-case filenames
|
|
435
|
+
│ └── my-other-thing.gjs
|
|
436
|
+
└── lib/
|
|
437
|
+
└── my-utils.js # pure helpers, no Ember dependency
|
|
438
|
+
|
|
439
|
+
assets/javascripts/admin/
|
|
440
|
+
├── components/
|
|
441
|
+
│ └── admin-my-plugin.gjs # admin components prefix with "admin-"
|
|
442
|
+
└── routes/
|
|
443
|
+
└── admin-plugins-my-plugin.js
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Practical Checklist
|
|
447
|
+
|
|
448
|
+
- [ ] Using `.gjs` (not `.hbs`/`.js` pairs) for new components
|
|
449
|
+
- [ ] Entry point is `apiInitializer`, not a bare `withPluginApi` export
|
|
450
|
+
- [ ] Plugin outlets via `api.renderInOutlet` — not `decorateWidget`
|
|
451
|
+
- [ ] `@tracked` only for state the component owns — not derived values
|
|
452
|
+
- [ ] Derived values are getters, not `@tracked` properties
|
|
453
|
+
- [ ] Services injected via `@service` — not imported as singletons
|
|
454
|
+
- [ ] AJAX via `discourse/lib/ajax` — not raw `fetch`
|
|
455
|
+
- [ ] Errors handled with `popupAjaxError`
|
|
456
|
+
- [ ] No raw HTML output with user-supplied content
|
|
457
|
+
- [ ] Backend enforces all authorization — client checks are UI-only
|
|
458
|
+
|
|
459
|
+
## Quick Reference: Common Imports
|
|
460
|
+
|
|
461
|
+
```javascript
|
|
462
|
+
// Glimmer/Ember core
|
|
463
|
+
import Component from "@glimmer/component";
|
|
464
|
+
import { tracked } from "@glimmer/tracking";
|
|
465
|
+
import { action } from "@ember/object";
|
|
466
|
+
import { service } from "@ember/service";
|
|
467
|
+
|
|
468
|
+
// Discourse plugin API
|
|
469
|
+
import { apiInitializer } from "discourse/lib/api";
|
|
470
|
+
import { ajax } from "discourse/lib/ajax";
|
|
471
|
+
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
472
|
+
|
|
473
|
+
// Discourse components
|
|
474
|
+
import DButton from "discourse/components/d-button";
|
|
475
|
+
import LoadingSpinner from "discourse/components/loading-spinner";
|
|
476
|
+
import DModal from "discourse/components/d-modal";
|
|
477
|
+
|
|
478
|
+
// i18n
|
|
479
|
+
import { i18n } from "discourse-i18n"; // current (replaces the old I18n.t())
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## When to Load Reference Files
|
|
483
|
+
|
|
484
|
+
### Admin UI Patterns
|
|
485
|
+
**File**: [`references/admin-ui.md`](references/admin-ui.md)
|
|
486
|
+
**Load when**: Building admin routes, tables, forms, settings UI
|
|
487
|
+
**Contains**: Full admin route + component example, admin table patterns, settings form
|
|
488
|
+
|
|
489
|
+
### Plugin Outlet Reference
|
|
490
|
+
**File**: [`references/outlets.md`](references/outlets.md)
|
|
491
|
+
**Load when**: Need to find the right outlet or understand outletArgs context
|
|
492
|
+
**Contains**: Common outlet names, outletArgs by context, shouldRender patterns
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
**Evidence Base**: Discourse Developer Docs (2025), Discourse Meta dev posts (Glimmer post stream migration Q1 2025, topic list migration), Ember Octane Guides, Discourse CVE history.
|