ember-tribe 2.6.3 → 2.6.5
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 +8 -21
- package/blueprints/ember-tribe/files/README.md +3 -0
- package/blueprints/ember-tribe/files/php-dist +134 -0
- package/blueprints/ember-tribe/files/{lib/storylang/pull.js → storylang} +68 -168
- package/package.json +1 -1
- package/blueprints/ember-tribe/files/bin/commands/pull.js +0 -15
- package/blueprints/ember-tribe/files/bin/commands/push.js +0 -24
- package/blueprints/ember-tribe/files/bin/storylang +0 -4
- package/blueprints/ember-tribe/files/lib/storylang/push.js +0 -403
- package/blueprints/ember-tribe/files/sync-dist.php +0 -58
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
|
|
60
|
+
node storylang
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* php-dist — single-file edition
|
|
6
|
+
*
|
|
7
|
+
* Usage: node php-dist
|
|
8
|
+
*
|
|
9
|
+
* Reads dist/index.html, injects PHP includes, strips <title> and
|
|
10
|
+
* <meta name="description">, prepends _init.php, and writes dist/index.php.
|
|
11
|
+
* Replicates the behaviour of `php sync-dist.php`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Minimal colour helpers — same feel as the PHP tput calls */
|
|
22
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
23
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
24
|
+
const hr = '~~~~~~~~~~';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Remove every occurrence of a tag (and its contents) that matches a
|
|
28
|
+
* predicate. We do this with a regex-free string walk so we never need
|
|
29
|
+
* a full DOM parser dependency.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} html
|
|
32
|
+
* @param {string} tagName — lower-case tag name, e.g. 'title' or 'meta'
|
|
33
|
+
* @param {(attrs: string) => boolean} predicate
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function removeTags(html, tagName, predicate) {
|
|
37
|
+
const result = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
|
|
40
|
+
while (i < html.length) {
|
|
41
|
+
// Look for the opening '<'
|
|
42
|
+
const openAngle = html.indexOf('<', i);
|
|
43
|
+
if (openAngle === -1) { result.push(html.slice(i)); break; }
|
|
44
|
+
|
|
45
|
+
// Collect everything up to this '<'
|
|
46
|
+
result.push(html.slice(i, openAngle));
|
|
47
|
+
|
|
48
|
+
// Find the end of this tag
|
|
49
|
+
const closeAngle = html.indexOf('>', openAngle);
|
|
50
|
+
if (closeAngle === -1) { result.push(html.slice(openAngle)); break; }
|
|
51
|
+
|
|
52
|
+
const rawTag = html.slice(openAngle + 1, closeAngle); // e.g. 'meta name="description" ...'
|
|
53
|
+
const isSelfClosing = rawTag.endsWith('/');
|
|
54
|
+
const tagBody = isSelfClosing ? rawTag.slice(0, -1).trim() : rawTag.trim();
|
|
55
|
+
const spaceIdx = tagBody.search(/[\s/]/);
|
|
56
|
+
const tag = (spaceIdx === -1 ? tagBody : tagBody.slice(0, spaceIdx)).toLowerCase();
|
|
57
|
+
const attrs = spaceIdx === -1 ? '' : tagBody.slice(spaceIdx);
|
|
58
|
+
|
|
59
|
+
if (tag === tagName && predicate(attrs)) {
|
|
60
|
+
// For void / self-closing elements (like <meta>) there is no closing tag.
|
|
61
|
+
const voidElements = new Set(['area','base','br','col','embed','hr','img','input',
|
|
62
|
+
'link','meta','param','source','track','wbr']);
|
|
63
|
+
if (voidElements.has(tagName) || isSelfClosing) {
|
|
64
|
+
// Just skip the tag entirely — move past '>'
|
|
65
|
+
i = closeAngle + 1;
|
|
66
|
+
} else {
|
|
67
|
+
// Skip tag + inner content + closing tag
|
|
68
|
+
const closing = `</${tagName}>`;
|
|
69
|
+
const closeIdx = html.toLowerCase().indexOf(closing, closeAngle + 1);
|
|
70
|
+
if (closeIdx === -1) {
|
|
71
|
+
// No closing tag found — skip just the opening tag
|
|
72
|
+
i = closeAngle + 1;
|
|
73
|
+
} else {
|
|
74
|
+
i = closeIdx + closing.length;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// Not a match — keep it
|
|
79
|
+
result.push(`<${rawTag}>`);
|
|
80
|
+
i = closeAngle + 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result.join('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Main
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
(function main() {
|
|
92
|
+
const cwd = process.cwd();
|
|
93
|
+
const inputFile = path.join(cwd, 'dist', 'index.html');
|
|
94
|
+
const outputFile = path.join(cwd, 'dist', 'index.php');
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(inputFile)) {
|
|
97
|
+
console.log(hr);
|
|
98
|
+
console.log(red('Run "ember build -prod" before syncing with PHP.'));
|
|
99
|
+
console.log(hr);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let html = fs.readFileSync(inputFile, 'utf8');
|
|
104
|
+
|
|
105
|
+
// Inject PHP includes ---------------------------------------------------
|
|
106
|
+
|
|
107
|
+
// Before <title>: _head.php
|
|
108
|
+
html = html.replace('<title>', '<?php include_once("_head.php");?><title>');
|
|
109
|
+
|
|
110
|
+
// Before </head>: _head_footer.php
|
|
111
|
+
html = html.replace('</head>', '<?php include_once("_head_footer.php");?></head>');
|
|
112
|
+
|
|
113
|
+
// Before </body>: _body_footer.php
|
|
114
|
+
html = html.replace('</body>', '<?php include_once("_body_footer.php");?></body>');
|
|
115
|
+
|
|
116
|
+
// Remove <meta name="description"> ------------------------------------
|
|
117
|
+
html = removeTags(html, 'meta', (attrs) => {
|
|
118
|
+
// Match name="description" or name='description'
|
|
119
|
+
return /name\s*=\s*["']description["']/i.test(attrs);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Remove <title>…</title> --------------------------------------------
|
|
123
|
+
html = removeTags(html, 'title', () => true);
|
|
124
|
+
|
|
125
|
+
// Prepend _init.php ---------------------------------------------------
|
|
126
|
+
html = '<?php include_once("_init.php");?>' + html;
|
|
127
|
+
|
|
128
|
+
// Write output --------------------------------------------------------
|
|
129
|
+
fs.writeFileSync(outputFile, html, 'utf8');
|
|
130
|
+
|
|
131
|
+
console.log(hr);
|
|
132
|
+
console.log(green('Middleware successfully installed. Synced "/dist" folder with PHP.'));
|
|
133
|
+
console.log(hr);
|
|
134
|
+
})();
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* storylang
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
135
|
+
const componentMap = new Map();
|
|
136
|
+
for (const jsFile of jsFiles) {
|
|
173
137
|
const name = nameFromPath(jsFile, appDir, 'components');
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
210
|
+
// Run
|
|
285
211
|
// ---------------------------------------------------------------------------
|
|
286
212
|
|
|
287
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
320
|
-
types: mergeByKey(existing.types || [], types, 'slug'),
|
|
239
|
+
types: mergeByKey(existing.types || [], types, 'slug'),
|
|
321
240
|
components: mergeByKey(existing.components || [], components, 'name'),
|
|
322
|
-
routes:
|
|
323
|
-
services:
|
|
324
|
-
helpers:
|
|
325
|
-
modifiers:
|
|
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,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
|
-
};
|
|
@@ -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
|
-
};
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
if (file_exists('dist/index.html') !== false) {
|
|
3
|
-
//get html
|
|
4
|
-
$html = file_get_contents('dist/index.html');
|
|
5
|
-
|
|
6
|
-
//replace head, before title
|
|
7
|
-
$html = str_replace('<title>', '<?php include_once("_head.php");?><title>', $html);
|
|
8
|
-
|
|
9
|
-
//replace head-footer, before </head> ends
|
|
10
|
-
$html = str_replace('</head>', '<?php include_once("_head_footer.php");?></head>', $html);
|
|
11
|
-
|
|
12
|
-
//replace body-footer, before </body> ends
|
|
13
|
-
$html = str_replace('</body>', '<?php include_once("_body_footer.php");?></body>', $html);
|
|
14
|
-
|
|
15
|
-
//load dom
|
|
16
|
-
$dom = new DOMDocument();
|
|
17
|
-
$dom->loadHTML($html);
|
|
18
|
-
|
|
19
|
-
//for tags to be removed
|
|
20
|
-
$remove = [];
|
|
21
|
-
|
|
22
|
-
//remove meta description tag
|
|
23
|
-
$metas = $dom->getElementsByTagName('meta');
|
|
24
|
-
for($i=0; $i <$metas-> length; $i++) {
|
|
25
|
-
$name = $metas->item($i)->getAttribute("name");
|
|
26
|
-
if ($name == 'description')
|
|
27
|
-
$remove[] = $metas->item($i);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
//remove title tag
|
|
31
|
-
$titles = $dom->getElementsByTagName('title');
|
|
32
|
-
foreach($titles as $item)
|
|
33
|
-
$remove[] = $item;
|
|
34
|
-
|
|
35
|
-
//remove identified tags
|
|
36
|
-
foreach ($remove as $item)
|
|
37
|
-
$item->parentNode->removeChild($item);
|
|
38
|
-
|
|
39
|
-
//save edit dom html
|
|
40
|
-
$html = $dom->saveHTML();
|
|
41
|
-
|
|
42
|
-
//prepend _init.php for $type and $slug and Tribe composer
|
|
43
|
-
$html = '<?php include_once("_init.php");?>'.$html;
|
|
44
|
-
|
|
45
|
-
//save to file
|
|
46
|
-
file_put_contents('dist/index.php', $html);
|
|
47
|
-
|
|
48
|
-
//echo success message on commandline
|
|
49
|
-
echo '~~~~~~~~~~'."\r\n";
|
|
50
|
-
echo `tput setaf 2`.'Middleware successfully installed. Synced "/dist" folder with PHP.'.`tput sgr0`."\r\n";
|
|
51
|
-
echo '~~~~~~~~~~'."\r\n";
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
echo '~~~~~~~~~~'."\r\n";
|
|
55
|
-
echo `tput setaf 1`.'Run "ember build -prod" before syncing with PHP.'.`tput sgr0`."\r\n";
|
|
56
|
-
echo '~~~~~~~~~~'."\r\n";
|
|
57
|
-
}
|
|
58
|
-
?>
|