ember-tribe 2.6.2 → 2.6.4

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/README.md CHANGED
@@ -57,36 +57,23 @@ ember-tribe ships with a command-line tool called `storylang` that synchronises
57
57
  ### Usage
58
58
 
59
59
  ```bash
60
- storylang <command> [options]
60
+ node storylang
61
61
  ```
62
62
 
63
- **Commands:**
64
-
65
- | Command | Description |
66
- |---|---|
67
- | `storylang pull` | Updates `config/storylang.json` from current project files |
68
- | `storylang push` | Generates any missing routes, components, services, helpers, modifiers, and controllers |
69
- | `storylang push -o` | Regenerates all artefacts in `storylang.json`, overwriting existing files |
63
+ Scans the current Ember project and writes/updates `config/storylang.json` from the actual files that exist in the `app/` directory.
70
64
 
71
65
  **Example:**
72
66
 
73
67
  ```bash
74
68
  cd /path/to/ember/app
75
69
 
76
- storylang pull
70
+ node storylang
77
71
  # => config/storylang.json updated from project files
78
-
79
- storylang push
80
- # => missing routes, components, services, helpers, modifiers, and controllers are generated
81
-
82
- storylang push -o
83
- # => all artefacts in storylang.json are (re)generated, overwriting existing files
84
72
  ```
85
73
 
86
74
  ### Typical Workflow
87
75
 
88
- 1. Define your `config/storylang.json` spec (manually or with the help of an AI tool), then run `storylang push` to generate the project files.
89
- 2. Run `storylang pull` periodically to keep the spec in sync as the project evolves.
76
+ Run `node storylang` periodically to keep `config/storylang.json` in sync as the project evolves.
90
77
 
91
78
  ---
92
79
 
@@ -796,10 +783,10 @@ export default class UserCard extends Component {
796
783
  ```javascript
797
784
  // app/helpers/format-currency.js
798
785
  export default function formatCurrency(amount, currency = 'USD') {
799
- return new Intl.NumberFormat('en-US', {
800
- style: 'currency',
801
- currency: currency,
802
- }).format(amount);
786
+ return new Intl.NumberFormat('en-US', {
787
+ style: 'currency',
788
+ currency: currency,
789
+ }).format(amount);
803
790
  }
804
791
  ```
805
792
 
@@ -0,0 +1,5 @@
1
+ <section class="flame-bg d-flex align-items-center justify-content-center">
2
+ <div class="py-6 container px-0 text-center text-dark">
3
+ <img src="/assets/img/flame.png" width="200">
4
+ </div>
5
+ </section>
@@ -0,0 +1,2 @@
1
+ {{page-title "<%= classifiedPackageName %>"}}
2
+ {{outlet}}
@@ -0,0 +1,7 @@
1
+ {{!-- Remove the WelcomeFlame component and write your HTML code here --}}
2
+
3
+ <WelcomeFlame />
4
+
5
+ {{!-- / --}}
6
+
7
+ {{outlet}}
@@ -1,7 +1,10 @@
1
+ #!/usr/bin/env node
1
2
  'use strict';
2
3
 
3
4
  /**
4
- * storylang pull
5
+ * storylang — single-file edition
6
+ *
7
+ * Usage: node tribe storylang
5
8
  *
6
9
  * Scans the current Ember project and writes/updates config/storylang.json
7
10
  * from the actual files that exist in the app/ directory.
@@ -18,9 +21,13 @@ function toCamelCase(str) {
18
21
  return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
19
22
  }
20
23
 
21
- /**
22
- * Walk a directory and return all files (recursively) that pass the filter fn.
23
- */
24
+ function toKebabCase(str) {
25
+ return str
26
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
27
+ .replace(/([a-z\d])([A-Z])/g, '$1-$2')
28
+ .toLowerCase();
29
+ }
30
+
24
31
  function walkDir(dir, filterFn = () => true) {
25
32
  if (!fs.existsSync(dir)) return [];
26
33
  const results = [];
@@ -35,13 +42,8 @@ function walkDir(dir, filterFn = () => true) {
35
42
  return results;
36
43
  }
37
44
 
38
- /**
39
- * Given a file path inside app/<category>/, derive a kebab-case name
40
- * suitable for storylang.json.
41
- */
42
45
  function nameFromPath(filePath, appDir, category) {
43
46
  const rel = path.relative(path.join(appDir, category), filePath);
44
- // strip extension(s) and turn path separators into slashes (nested components)
45
47
  return rel
46
48
  .replace(/\.(js|ts|hbs|scss|css)$/, '')
47
49
  .split(path.sep)
@@ -49,35 +51,24 @@ function nameFromPath(filePath, appDir, category) {
49
51
  }
50
52
 
51
53
  // ---------------------------------------------------------------------------
52
- // Parsers — extract metadata from source files
54
+ // Parsers
53
55
  // ---------------------------------------------------------------------------
54
56
 
55
- /**
56
- * Very lightweight static analysis: look for @tracked, @action, @service,
57
- * imported helpers / modifiers, and inherited args from HBS counterpart.
58
- */
59
57
  function parseJsFile(filePath) {
60
58
  const src = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
61
59
 
62
- // @tracked varName
63
60
  const trackedVars = [...src.matchAll(/@tracked\s+(\w+)\s*(?:=\s*([^;]+))?/g)].map(
64
- ([, name, val]) => {
65
- const type = inferType(val);
66
- return { [name]: type };
67
- }
61
+ ([, name, val]) => ({ [toKebabCase(name)]: inferType(val) })
68
62
  );
69
63
 
70
- // @action methodName(
71
64
  const actions = [...src.matchAll(/@action\s*\n?\s*(?:async\s+)?(\w+)\s*\(/g)].map(
72
- ([, name]) => name
65
+ ([, name]) => toKebabCase(name)
73
66
  );
74
67
 
75
- // @service serviceName;
76
- const services = [...src.matchAll(/@service\s+(\w+)/g)].map(([, name]) => name);
68
+ const services = [...src.matchAll(/@service\s+(\w+)/g)].map(([, name]) => toKebabCase(name));
77
69
 
78
- // queryParams = { key: ... }
79
70
  const getVars = [...src.matchAll(/queryParams\s*=\s*\{([^}]+)\}/gs)].flatMap(([, block]) =>
80
- [...block.matchAll(/(\w+)\s*:/g)].map(([, k]) => ({ [k]: 'string' }))
71
+ [...block.matchAll(/(\w+)\s*:/g)].map(([, k]) => ({ [toKebabCase(k)]: 'string' }))
81
72
  );
82
73
 
83
74
  return { trackedVars, actions, services, getVars };
@@ -87,34 +78,24 @@ function inferType(val = '') {
87
78
  val = (val || '').trim();
88
79
  if (val === 'false' || val === 'true') return 'bool';
89
80
  if (val.startsWith('[')) return 'array';
90
- if (val.startsWith('{') || val.startsWith('new Map') || val.startsWith('new Set'))
91
- return 'object';
81
+ if (val.startsWith('{') || val.startsWith('new Map') || val.startsWith('new Set')) return 'object';
92
82
  if (/^\d+$/.test(val)) return 'int';
93
83
  if (val.startsWith("'") || val.startsWith('"') || val.startsWith('`')) return 'string';
94
84
  return 'string';
95
85
  }
96
86
 
97
- /**
98
- * Parse a Handlebars template for:
99
- * - @arg references → inherited_args
100
- * - <ComponentName> invocations → sub-components used
101
- * - {{helper-name}} calls → helpers used
102
- * - {{modifier}} element modifiers → modifiers used
103
- */
104
87
  function parseHbsFile(filePath) {
105
88
  if (!fs.existsSync(filePath)) return { inheritedArgs: [], helpers: [], modifiers: [], components: [] };
106
89
 
107
90
  const src = fs.readFileSync(filePath, 'utf8');
108
91
 
109
- // @argName (distinct, excluding @glimmer internals)
110
92
  const inheritedArgsSet = new Set(
111
93
  [...src.matchAll(/@(\w[\w.]*)/g)]
112
94
  .map(([, name]) => name.split('.')[0])
113
95
  .filter((n) => !['ember', 'glimmer'].includes(n))
114
96
  );
115
- const inheritedArgs = [...inheritedArgsSet].map((name) => ({ [name]: 'var' }));
97
+ const inheritedArgs = [...inheritedArgsSet].map((name) => ({ [toKebabCase(name)]: 'var' }));
116
98
 
117
- // {{some-helper ...}} — exclude built-ins
118
99
  const builtinHelpers = new Set([
119
100
  'if', 'unless', 'each', 'let', 'with', 'yield', 'outlet', 'component',
120
101
  'on', 'get', 'concat', 'array', 'hash', 'log', 'action', 'mut',
@@ -127,40 +108,21 @@ function parseHbsFile(filePath) {
127
108
  );
128
109
  const helpers = [...helpersSet];
129
110
 
130
- // {{modifier-name}} inside element modifier position — {{some-modifier ...}}
131
111
  const modifiersSet = new Set(
132
112
  [...src.matchAll(/\{\{([\w-]+-modifier|autofocus|tooltip|play-when|[\w-]+)\s/g)]
133
113
  .map(([, name]) => name)
134
- .filter((n) => helpers.includes(n) === false && !builtinHelpers.has(n))
114
+ .filter((n) => !helpers.includes(n) && !builtinHelpers.has(n))
135
115
  );
136
116
  const modifiers = [...modifiersSet];
137
117
 
138
- // <ComponentName (PascalCase or path-style)
139
118
  const componentsSet = new Set(
140
- [...src.matchAll(/<([A-Z][\w::/]*)/g)].map(([, name]) => name)
119
+ [...src.matchAll(/<([A-Z][\w::/]*)/g)].map(([, name]) => toKebabCase(name))
141
120
  );
142
121
  const components = [...componentsSet];
143
122
 
144
123
  return { inheritedArgs, helpers, modifiers, components };
145
124
  }
146
125
 
147
- /**
148
- * Sniff the type of component from its name, matching README built-in types.
149
- */
150
- function guessComponentType(name) {
151
- const LAYOUT = ['table', 'figure', 'accordion', 'card', 'list-group', 'navbar', 'nav', 'tab', 'breadcrumb'];
152
- const INTERACTIVE = [
153
- 'button', 'button-group', 'dropdown', 'modal', 'collapse', 'offcanvas', 'pagination',
154
- 'popover', 'tooltip', 'swiper-carousel', 'videojs-player', 'howlerjs-player', 'input-field',
155
- 'input-group', 'textarea', 'checkbox', 'radio', 'range', 'select', 'multi-select', 'date',
156
- 'file-uploader', 'alert', 'badge', 'toast', 'placeholder', 'progress', 'spinner', 'scrollspy',
157
- ];
158
- for (const t of [...LAYOUT, ...INTERACTIVE]) {
159
- if (name.includes(t)) return t;
160
- }
161
- return 'component';
162
- }
163
-
164
126
  // ---------------------------------------------------------------------------
165
127
  // Section builders
166
128
  // ---------------------------------------------------------------------------
@@ -168,54 +130,37 @@ function guessComponentType(name) {
168
130
  function buildComponents(appDir) {
169
131
  const componentDir = path.join(appDir, 'components');
170
132
  const jsFiles = walkDir(componentDir, (n) => /\.(js|ts)$/.test(n));
133
+ const hbsFiles = walkDir(componentDir, (n) => /\.hbs$/.test(n));
171
134
 
172
- return jsFiles.map((jsFile) => {
135
+ const componentMap = new Map();
136
+ for (const jsFile of jsFiles) {
173
137
  const name = nameFromPath(jsFile, appDir, 'components');
174
- const hbsFile = jsFile.replace(/\.(js|ts)$/, '.hbs');
138
+ componentMap.set(name, { ...componentMap.get(name), jsFile });
139
+ }
140
+ for (const hbsFile of hbsFiles) {
141
+ const name = nameFromPath(hbsFile, appDir, 'components');
142
+ componentMap.set(name, { ...componentMap.get(name), hbsFile });
143
+ }
144
+
145
+ return [...componentMap.entries()].map(([name, { jsFile, hbsFile }]) => {
175
146
  const { trackedVars, actions, services } = parseJsFile(jsFile);
176
147
  const { inheritedArgs, helpers, modifiers } = parseHbsFile(hbsFile);
177
-
178
- return {
179
- name,
180
- type: guessComponentType(name),
181
- tracked_vars: trackedVars,
182
- inherited_args: inheritedArgs,
183
- actions,
184
- helpers,
185
- modifiers,
186
- services,
187
- };
148
+ return { name, tracked_vars: trackedVars, inherited_args: inheritedArgs, actions, helpers, modifiers, services };
188
149
  });
189
150
  }
190
151
 
191
152
  function buildRoutes(appDir) {
192
- const routeDir = path.join(appDir, 'routes');
193
- const jsFiles = walkDir(routeDir, (n) => /\.(js|ts)$/.test(n));
194
-
195
- return jsFiles.map((jsFile) => {
153
+ return walkDir(path.join(appDir, 'routes'), (n) => /\.(js|ts)$/.test(n)).map((jsFile) => {
196
154
  const name = nameFromPath(jsFile, appDir, 'routes');
197
155
  const hbsFile = path.join(appDir, 'templates', name.replace(/\/$/, '') + '.hbs');
198
156
  const { trackedVars, actions, services, getVars } = parseJsFile(jsFile);
199
157
  const { helpers, components } = parseHbsFile(hbsFile);
200
-
201
- return {
202
- name,
203
- tracked_vars: trackedVars,
204
- get_vars: getVars,
205
- actions,
206
- helpers,
207
- services,
208
- components,
209
- types: [],
210
- };
158
+ return { name, tracked_vars: trackedVars, get_vars: getVars, actions, helpers, services, components, types: [] };
211
159
  });
212
160
  }
213
161
 
214
162
  function buildServices(appDir) {
215
- const serviceDir = path.join(appDir, 'services');
216
- const jsFiles = walkDir(serviceDir, (n) => /\.(js|ts)$/.test(n));
217
-
218
- return jsFiles.map((jsFile) => {
163
+ return walkDir(path.join(appDir, 'services'), (n) => /\.(js|ts)$/.test(n)).map((jsFile) => {
219
164
  const name = nameFromPath(jsFile, appDir, 'services');
220
165
  const { trackedVars, actions, services } = parseJsFile(jsFile);
221
166
  return { name, tracked_vars: trackedVars, actions, services };
@@ -223,106 +168,80 @@ function buildServices(appDir) {
223
168
  }
224
169
 
225
170
  function buildHelpers(appDir) {
226
- const helperDir = path.join(appDir, 'helpers');
227
- const jsFiles = walkDir(helperDir, (n) => /\.(js|ts)$/.test(n));
228
-
229
- return jsFiles.map((jsFile) => {
171
+ return walkDir(path.join(appDir, 'helpers'), (n) => /\.(js|ts)$/.test(n)).map((jsFile) => {
230
172
  const name = nameFromPath(jsFile, appDir, 'helpers');
231
173
  const src = fs.readFileSync(jsFile, 'utf8');
232
-
233
- // Infer args from function signature
234
174
  const sig = src.match(/function\s+\w*\s*\(\[([^\]]*)\](?:,\s*\{([^}]*)\})?\)/);
235
- const posArgs = sig ? sig[1].split(',').map((s) => s.trim()).filter(Boolean).map((a) => ({ [a]: 'string' })) : [];
175
+ const posArgs = sig ? sig[1].split(',').map((s) => s.trim()).filter(Boolean).map((a) => ({ [toKebabCase(a)]: 'string' })) : [];
236
176
  const namedArgs = sig && sig[2]
237
- ? sig[2].split(',').map((s) => s.trim().split(/\s*=\s*/)[0]).filter(Boolean).map((a) => ({ [a]: 'string' }))
177
+ ? sig[2].split(',').map((s) => s.trim().split(/\s*=\s*/)[0]).filter(Boolean).map((a) => ({ [toKebabCase(a)]: 'string' }))
238
178
  : [];
239
-
240
- return {
241
- name,
242
- description: '',
243
- args: [...posArgs, ...namedArgs],
244
- return: 'string',
245
- };
179
+ return { name, args: [...posArgs, ...namedArgs], return: 'string' };
246
180
  });
247
181
  }
248
182
 
249
183
  function buildModifiers(appDir) {
250
- const modifierDir = path.join(appDir, 'modifiers');
251
- const jsFiles = walkDir(modifierDir, (n) => /\.(js|ts)$/.test(n));
252
-
253
- return jsFiles.map((jsFile) => {
184
+ return walkDir(path.join(appDir, 'modifiers'), (n) => /\.(js|ts)$/.test(n)).map((jsFile) => {
254
185
  const name = nameFromPath(jsFile, appDir, 'modifiers');
255
186
  const { services } = parseJsFile(jsFile);
256
- return { name, description: '', args: [], services };
187
+ return { name, args: [], services };
257
188
  });
258
189
  }
259
190
 
260
- /**
261
- * Build the types section by cross-referencing component/route usages.
262
- */
263
- function buildTypes(routes, components, services) {
191
+ function buildTypes(routes, components) {
264
192
  const typeMap = new Map();
265
-
266
- function register(typeSlug, category, name) {
267
- if (!typeMap.has(typeSlug)) {
268
- typeMap.set(typeSlug, { routes: [], components: [], services: [], helpers: [], modifiers: [] });
269
- }
193
+ const register = (typeSlug, category, name) => {
194
+ if (!typeMap.has(typeSlug)) typeMap.set(typeSlug, { routes: [], components: [], services: [], helpers: [], modifiers: [] });
270
195
  typeMap.get(typeSlug)[category].push(name);
271
- }
272
-
273
- for (const r of routes) {
274
- for (const t of r.types || []) register(t, 'routes', r.name);
275
- }
276
- for (const c of components) {
277
- for (const t of c.types || []) register(t, 'components', c.name);
278
- }
279
-
196
+ };
197
+ for (const r of routes) for (const t of r.types || []) register(t, 'routes', r.name);
198
+ for (const c of components) for (const t of c.types || []) register(t, 'components', c.name);
280
199
  return [...typeMap.entries()].map(([slug, used_in]) => ({ slug, used_in }));
281
200
  }
282
201
 
202
+ function mergeByKey(existing, scanned, key) {
203
+ const existingMap = new Map(existing.map((e) => [e[key], e]));
204
+ const scannedMap = new Map(scanned.map((s) => [s[key], s]));
205
+ const allKeys = new Set([...existingMap.keys(), ...scannedMap.keys()]);
206
+ return [...allKeys].map((k) => ({ ...(scannedMap.get(k) || {}) }));
207
+ }
208
+
283
209
  // ---------------------------------------------------------------------------
284
- // Main export
210
+ // Run
285
211
  // ---------------------------------------------------------------------------
286
212
 
287
- module.exports = async function pull(cwd) {
213
+ (async () => {
214
+ const cwd = process.cwd();
288
215
  const appDir = path.join(cwd, 'app');
289
216
  const configDir = path.join(cwd, 'config');
290
217
  const outputFile = path.join(configDir, 'storylang.json');
291
218
 
292
219
  if (!fs.existsSync(appDir)) {
293
- throw new Error(
294
- `Could not find app/ directory at ${appDir}. ` +
295
- `Make sure you are running storylang from the root of your Ember project.`
296
- );
220
+ console.error(`Could not find app/ directory at ${appDir}. Make sure you are running from the root of your Ember project.`);
221
+ process.exit(1);
297
222
  }
298
223
 
299
- console.log('📖 storylang pull — scanning project files…\n');
224
+ console.log('📖 storylang — scanning project files…\n');
300
225
 
301
226
  const components = buildComponents(appDir);
302
227
  const routes = buildRoutes(appDir);
303
228
  const services = buildServices(appDir);
304
229
  const helpers = buildHelpers(appDir);
305
230
  const modifiers = buildModifiers(appDir);
306
- const types = buildTypes(routes, components, services);
231
+ const types = buildTypes(routes, components);
307
232
 
308
- // Merge with existing storylang.json so hand-written fields are preserved
309
233
  let existing = {};
310
234
  if (fs.existsSync(outputFile)) {
311
- try {
312
- existing = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
313
- } catch (_) {
314
- // corrupt file — start fresh
315
- }
235
+ try { existing = JSON.parse(fs.readFileSync(outputFile, 'utf8')); } catch (_) {}
316
236
  }
317
237
 
318
238
  const merged = {
319
- implementation_approach: existing.implementation_approach || '',
320
- types: mergeByKey(existing.types || [], types, 'slug'),
239
+ types: mergeByKey(existing.types || [], types, 'slug'),
321
240
  components: mergeByKey(existing.components || [], components, 'name'),
322
- routes: mergeByKey(existing.routes || [], routes, 'name'),
323
- services: mergeByKey(existing.services || [], services, 'name'),
324
- helpers: mergeByKey(existing.helpers || [], helpers, 'name'),
325
- modifiers: mergeByKey(existing.modifiers || [], modifiers, 'name'),
241
+ routes: mergeByKey(existing.routes || [], routes, 'name'),
242
+ services: mergeByKey(existing.services || [], services, 'name'),
243
+ helpers: mergeByKey(existing.helpers || [], helpers, 'name'),
244
+ modifiers: mergeByKey(existing.modifiers || [], modifiers, 'name'),
326
245
  };
327
246
 
328
247
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
@@ -335,23 +254,4 @@ module.exports = async function pull(cwd) {
335
254
  console.log(` helpers: ${helpers.length}`);
336
255
  console.log(` modifiers: ${modifiers.length}`);
337
256
  console.log(` types: ${types.length}`);
338
- };
339
-
340
- /**
341
- * Merge two arrays by a key, preferring scanned values for structural fields
342
- * but keeping existing hand-written description / implementation_approach fields.
343
- */
344
- function mergeByKey(existing, scanned, key) {
345
- const existingMap = new Map(existing.map((e) => [e[key], e]));
346
- const scannedMap = new Map(scanned.map((s) => [s[key], s]));
347
-
348
- const allKeys = new Set([...existingMap.keys(), ...scannedMap.keys()]);
349
- const result = [];
350
- for (const k of allKeys) {
351
- const e = existingMap.get(k) || {};
352
- const s = scannedMap.get(k) || {};
353
- // scanned data wins for structural fields; preserve description from existing
354
- result.push({ ...s, description: e.description || s.description || '' });
355
- }
356
- return result;
357
- }
257
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-tribe",
3
- "version": "2.6.2",
3
+ "version": "2.6.4",
4
4
  "description": "The default blueprint for using Tribe API and Junction within EmberJS.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -1,15 +0,0 @@
1
- 'use strict';
2
-
3
- const pull = require('../../lib/storylang/pull');
4
-
5
- module.exports.command = 'pull';
6
-
7
- module.exports.describe = 'update config/storylang.json from current project files';
8
-
9
- module.exports.handler = async function handler() {
10
- try {
11
- await pull(process.cwd());
12
- } catch (err) {
13
- console.error(err);
14
- }
15
- };
@@ -1,24 +0,0 @@
1
- 'use strict';
2
-
3
- const push = require('../../lib/storylang/push');
4
-
5
- module.exports.command = 'push';
6
-
7
- module.exports.describe = 'generate missing Ember files from config/storylang.json';
8
-
9
- module.exports.builder = {
10
- overwrite: {
11
- alias: 'o',
12
- type: 'boolean',
13
- default: false,
14
- describe: 'overwrite existing files instead of skipping them',
15
- },
16
- };
17
-
18
- module.exports.handler = async function handler(argv) {
19
- try {
20
- await push(process.cwd(), { overwrite: argv.overwrite });
21
- } catch (err) {
22
- console.error(err);
23
- }
24
- };
package/bin/storylang DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- require('yargs').commandDir('commands').demandCommand().help().argv;
@@ -1,11 +0,0 @@
1
- import Component from '@glimmer/component';
2
-
3
- export default class WelcomeFlameComponent extends Component {
4
- <template>
5
- <section class="flame-bg d-flex align-items-center justify-content-center">
6
- <div class="py-6 container px-0 text-center text-dark">
7
- <img src="/assets/img/flame.png" width="200">
8
- </div>
9
- </section>
10
- </template>
11
- }
@@ -1,6 +0,0 @@
1
- import { pageTitle } from 'ember-page-title';
2
-
3
- <template>
4
- {{pageTitle "<%= classifiedPackageName %>"}}
5
- {{outlet}}
6
- </template>
@@ -1,11 +0,0 @@
1
- import WelcomeFlame from '../components/welcome-flame';
2
-
3
- <template>
4
- {{! Remove the WelcomeFlame component and write your HTML code here }}
5
-
6
- <WelcomeFlame />
7
-
8
- {{! / }}
9
-
10
- {{outlet}}
11
- </template>
@@ -1,403 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * storylang push [--overwrite / -o]
5
- *
6
- * Reads config/storylang.json and generates missing (or all, when -o is set)
7
- * Ember project files: routes, controllers, components, services, helpers,
8
- * modifiers, and their corresponding templates where applicable.
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- // ---------------------------------------------------------------------------
15
- // Small utilities
16
- // ---------------------------------------------------------------------------
17
-
18
- function toPascalCase(str) {
19
- return str
20
- .split(/[-/]/)
21
- .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
22
- .join('');
23
- }
24
-
25
- function toCamelCase(str) {
26
- const pascal = toPascalCase(str);
27
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
28
- }
29
-
30
- function ensureDir(filePath) {
31
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
32
- }
33
-
34
- /**
35
- * Write a file only if it does not exist (or overwrite=true).
36
- * Returns true if the file was actually written.
37
- */
38
- function writeFile(filePath, content, overwrite) {
39
- if (!overwrite && fs.existsSync(filePath)) return false;
40
- ensureDir(filePath);
41
- fs.writeFileSync(filePath, content, 'utf8');
42
- return true;
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Template generators
47
- // ---------------------------------------------------------------------------
48
-
49
- /**
50
- * Route JS file
51
- */
52
- function routeJs(def) {
53
- const className = toPascalCase(def.name) + 'Route';
54
- const services = def.services || [];
55
- const getVars = def.get_vars || [];
56
- const actions = def.actions || [];
57
-
58
- const serviceImports = services.length ? `import { service } from '@ember/service';\n` : '';
59
- const actionImport = actions.length ? `import { action } from '@ember/object';\n` : '';
60
-
61
- const serviceProps = services.map((s) => ` @service ${toCamelCase(s)};`).join('\n');
62
-
63
- const queryParamsBlock =
64
- getVars.length
65
- ? `\n queryParams = {\n${getVars
66
- .map((v) => {
67
- const key = Object.keys(v)[0];
68
- return ` ${key}: { refreshModel: true },`;
69
- })
70
- .join('\n')}\n };\n`
71
- : '';
72
-
73
- const modelArgs = getVars.length ? 'params' : '';
74
- const modelBody =
75
- getVars.length
76
- ? ` // TODO: use params to filter/paginate\n // return await this.store.query('type-name', { page: { offset: 0, limit: 10 } });`
77
- : ` // TODO: return model data\n // return await this.store.query('type-name', {});`;
78
-
79
- const actionMethods = actions
80
- .map(
81
- (a) =>
82
- `\n @action\n async ${a}() {\n // TODO: implement ${a}\n }`
83
- )
84
- .join('\n');
85
-
86
- return `import Route from '@ember/routing/route';
87
- ${serviceImports}${actionImport}
88
- export default class ${className} extends Route {
89
- ${serviceProps ? serviceProps + '\n' : ''}${queryParamsBlock}
90
- async model(${modelArgs}) {
91
- ${modelBody}
92
- }
93
- ${actionMethods}
94
- }
95
- `;
96
- }
97
-
98
- /**
99
- * Controller JS file (minimal — logic lives in components)
100
- */
101
- function controllerJs(def) {
102
- const className = toPascalCase(def.name) + 'Controller';
103
- return `import Controller from '@ember/controller';
104
-
105
- export default class ${className} extends Controller {
106
- // Minimal controller — keep business logic in components and services.
107
- }
108
- `;
109
- }
110
-
111
- /**
112
- * Route HBS template
113
- */
114
- function routeHbs(def) {
115
- const components = def.components || [];
116
- const inner = components.length
117
- ? components.map((c) => ` <${toPascalCase(c)} />`).join('\n')
118
- : ' {{outlet}}';
119
- return `<div class="route-${def.name}">\n${inner}\n</div>\n`;
120
- }
121
-
122
- /**
123
- * Component JS file
124
- */
125
- function componentJs(def) {
126
- const className = toPascalCase(def.name) + 'Component';
127
- const trackedVars = def.tracked_vars || [];
128
- const actions = def.actions || [];
129
- const services = def.services || [];
130
-
131
- const hasTracked = trackedVars.length > 0;
132
- const hasActions = actions.length > 0;
133
- const hasServices = services.length > 0;
134
-
135
- const imports = [
136
- `import Component from '@glimmer/component';`,
137
- hasTracked ? `import { tracked } from '@glimmer/tracking';` : '',
138
- hasActions ? `import { action } from '@ember/object';` : '',
139
- hasServices ? `import { service } from '@ember/service';` : '',
140
- ]
141
- .filter(Boolean)
142
- .join('\n');
143
-
144
- const serviceProps = services.map((s) => ` @service ${toCamelCase(s)};`).join('\n');
145
- const trackedProps = trackedVars
146
- .map((v) => {
147
- const [name, type] = Object.entries(v)[0];
148
- const defaultVal = defaultForType(type);
149
- return ` @tracked ${name} = ${defaultVal};`;
150
- })
151
- .join('\n');
152
-
153
- const actionMethods = actions
154
- .map(
155
- (a) =>
156
- `\n @action\n async ${a}() {\n // TODO: implement ${a}\n }`
157
- )
158
- .join('\n');
159
-
160
- return `${imports}
161
-
162
- export default class ${className} extends Component {
163
- ${serviceProps ? serviceProps + '\n' : ''}${trackedProps ? trackedProps + '\n' : ''}${actionMethods}
164
- }
165
- `;
166
- }
167
-
168
- function defaultForType(type) {
169
- switch (type) {
170
- case 'bool': return 'false';
171
- case 'int': return '0';
172
- case 'array': return '[]';
173
- case 'object': return 'null';
174
- default: return 'null';
175
- }
176
- }
177
-
178
- /**
179
- * Component HBS template
180
- */
181
- function componentHbs(def) {
182
- const type = def.type || 'component';
183
- const inheritedArgs = def.inherited_args || [];
184
-
185
- // Pick a sensible wrapper element/class based on component type
186
- const wrapperClass = bootstrapClass(type);
187
- const argDisplay = inheritedArgs
188
- .filter((a) => {
189
- const key = Object.keys(a)[0];
190
- const argType = Object.values(a)[0];
191
- return argType === 'var';
192
- })
193
- .map((a) => {
194
- const key = Object.keys(a)[0];
195
- return ` <p>{{@${key}}}</p>`;
196
- })
197
- .join('\n');
198
-
199
- return `<div class="${wrapperClass}">
200
- ${argDisplay || ' {{! TODO: add template content }}'}
201
- </div>
202
- `;
203
- }
204
-
205
- function bootstrapClass(type) {
206
- const map = {
207
- card: 'card',
208
- table: 'table-responsive',
209
- modal: 'modal',
210
- accordion: 'accordion',
211
- alert: 'alert',
212
- badge: 'badge',
213
- toast: 'toast',
214
- navbar: 'navbar navbar-expand-lg',
215
- nav: 'nav',
216
- tab: 'nav nav-tabs',
217
- breadcrumb: 'breadcrumb',
218
- 'list-group': 'list-group',
219
- button: 'btn btn-primary',
220
- 'button-group': 'btn-group',
221
- dropdown: 'dropdown',
222
- collapse: 'collapse',
223
- offcanvas: 'offcanvas',
224
- pagination: 'pagination',
225
- progress: 'progress',
226
- spinner: 'spinner-border',
227
- };
228
- return map[type] || 'container';
229
- }
230
-
231
- /**
232
- * Service JS file
233
- */
234
- function serviceJs(def) {
235
- const className = toPascalCase(def.name) + 'Service';
236
- const trackedVars = def.tracked_vars || [];
237
- const actions = def.actions || [];
238
- const services = def.services || [];
239
-
240
- const hasTracked = trackedVars.length > 0;
241
- const hasActions = actions.length > 0;
242
- const hasServices = services.length > 0;
243
-
244
- const imports = [
245
- `import Service from '@ember/service';`,
246
- hasTracked ? `import { tracked } from '@glimmer/tracking';` : '',
247
- hasActions ? `import { action } from '@ember/object';` : '',
248
- hasServices ? `import { service } from '@ember/service';` : '',
249
- ]
250
- .filter(Boolean)
251
- .join('\n');
252
-
253
- const serviceProps = services.map((s) => ` @service ${toCamelCase(s)};`).join('\n');
254
- const trackedProps = trackedVars
255
- .map((v) => {
256
- const [name, type] = Object.entries(v)[0];
257
- return ` @tracked ${name} = ${defaultForType(type)};`;
258
- })
259
- .join('\n');
260
-
261
- const actionMethods = actions
262
- .map(
263
- (a) =>
264
- `\n async ${a}() {\n // TODO: implement ${a}\n }`
265
- )
266
- .join('\n');
267
-
268
- return `${imports}
269
-
270
- export default class ${className} extends Service {
271
- ${serviceProps ? serviceProps + '\n' : ''}${trackedProps ? trackedProps + '\n' : ''}${actionMethods}
272
- }
273
- `;
274
- }
275
-
276
- /**
277
- * Helper JS file
278
- */
279
- function helperJs(def) {
280
- const fnName = toCamelCase(def.name);
281
- const argNames = (def.args || []).map((a) => Object.keys(a)[0]);
282
- const positional = argNames.length ? `[${argNames.join(', ')}]` : '[]';
283
- const description = def.description ? `// ${def.description}\n` : '';
284
-
285
- return `import { helper } from '@ember/component/helper';
286
-
287
- ${description}export default helper(function ${fnName}(${positional}/*, namedArgs*/) {
288
- // TODO: implement ${def.name}
289
- return '';
290
- });
291
- `;
292
- }
293
-
294
- /**
295
- * Modifier JS file
296
- */
297
- function modifierJs(def) {
298
- const argNames = (def.args || []).map((a) => Object.keys(a)[0]);
299
- const positional = argNames.length ? `[${argNames.join(', ')}]` : '[]';
300
- const services = def.services || [];
301
- const description = def.description ? `// ${def.description}\n` : '';
302
- const serviceImports = services.length ? `import { service } from '@ember/service';\n` : '';
303
-
304
- return `import { modifier } from 'ember-modifier';
305
- ${serviceImports}
306
- ${description}export default modifier((element, ${positional}/*, namedArgs*/) => {
307
- // TODO: implement ${def.name} modifier
308
-
309
- return () => {
310
- // cleanup
311
- };
312
- });
313
- `;
314
- }
315
-
316
- // ---------------------------------------------------------------------------
317
- // Main export
318
- // ---------------------------------------------------------------------------
319
-
320
- module.exports = async function push(cwd, { overwrite = false } = {}) {
321
- const configFile = path.join(cwd, 'config', 'storylang.json');
322
-
323
- if (!fs.existsSync(configFile)) {
324
- throw new Error(
325
- `config/storylang.json not found at ${configFile}.\n` +
326
- `Run "storylang pull" first, or create config/storylang.json manually.`
327
- );
328
- }
329
-
330
- let spec;
331
- try {
332
- spec = JSON.parse(fs.readFileSync(configFile, 'utf8'));
333
- } catch (e) {
334
- throw new Error(`Failed to parse config/storylang.json: ${e.message}`);
335
- }
336
-
337
- const appDir = path.join(cwd, 'app');
338
- const mode = overwrite ? '(overwrite mode)' : '(skip existing)';
339
- console.log(`🚀 storylang push ${mode}\n`);
340
-
341
- const stats = { written: 0, skipped: 0 };
342
-
343
- function write(filePath, content) {
344
- const wrote = writeFile(filePath, content, overwrite);
345
- if (wrote) {
346
- console.log(` ✔ ${path.relative(cwd, filePath)}`);
347
- stats.written++;
348
- } else {
349
- console.log(` – ${path.relative(cwd, filePath)} (skipped — already exists)`);
350
- stats.skipped++;
351
- }
352
- }
353
-
354
- // ── Routes ──────────────────────────────────────────────────────────────
355
- if (spec.routes && spec.routes.length) {
356
- console.log('Routes:');
357
- for (const def of spec.routes) {
358
- if (!def.name) continue;
359
- write(path.join(appDir, 'routes', `${def.name}.js`), routeJs(def));
360
- write(path.join(appDir, 'controllers', `${def.name}.js`), controllerJs(def));
361
- write(path.join(appDir, 'templates', `${def.name}.hbs`), routeHbs(def));
362
- }
363
- }
364
-
365
- // ── Components ──────────────────────────────────────────────────────────
366
- if (spec.components && spec.components.length) {
367
- console.log('\nComponents:');
368
- for (const def of spec.components) {
369
- if (!def.name) continue;
370
- write(path.join(appDir, 'components', `${def.name}.js`), componentJs(def));
371
- write(path.join(appDir, 'components', `${def.name}.hbs`), componentHbs(def));
372
- }
373
- }
374
-
375
- // ── Services ─────────────────────────────────────────────────────────────
376
- if (spec.services && spec.services.length) {
377
- console.log('\nServices:');
378
- for (const def of spec.services) {
379
- if (!def.name) continue;
380
- write(path.join(appDir, 'services', `${def.name}.js`), serviceJs(def));
381
- }
382
- }
383
-
384
- // ── Helpers ──────────────────────────────────────────────────────────────
385
- if (spec.helpers && spec.helpers.length) {
386
- console.log('\nHelpers:');
387
- for (const def of spec.helpers) {
388
- if (!def.name) continue;
389
- write(path.join(appDir, 'helpers', `${def.name}.js`), helperJs(def));
390
- }
391
- }
392
-
393
- // ── Modifiers ────────────────────────────────────────────────────────────
394
- if (spec.modifiers && spec.modifiers.length) {
395
- console.log('\nModifiers:');
396
- for (const def of spec.modifiers) {
397
- if (!def.name) continue;
398
- write(path.join(appDir, 'modifiers', `${def.name}.js`), modifierJs(def));
399
- }
400
- }
401
-
402
- console.log(`\n✅ Done — ${stats.written} file(s) written, ${stats.skipped} skipped.`);
403
- };