@dsplay/template-manifest 1.0.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/README.md +87 -0
- package/cli.js +29 -0
- package/core.js +352 -0
- package/package.json +28 -0
- package/vite-plugin.js +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @dsplay/template-manifest
|
|
2
|
+
|
|
3
|
+
A build-time scanner for [DSPLAY HTML templates](https://developers.dsplay.tv/docs/html-templates) ([DSPLAY - Digital Signage](https://dsplay.tv/)).
|
|
4
|
+
|
|
5
|
+
It statically scans a template's source for its `dsplay_template` variable reads and captures its `dsplay-data.js` mock as example data, producing two JSON files a template ships inside its `template.zip`:
|
|
6
|
+
|
|
7
|
+
- **`template-variables.json`** — the detected variables, e.g.:
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"variables": [
|
|
11
|
+
{ "key": "title", "type": "string", "default": "Default Value" },
|
|
12
|
+
{ "key": "expanded", "type": "boolean", "default": true },
|
|
13
|
+
{ "key": "logo", "type": "string", "subtypeGuess": "image" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
- **`template-example-data.json`** — the `{ config, media, template }` example values captured from `dsplay-data.js`.
|
|
18
|
+
|
|
19
|
+
The DSPLAY CMS reads these files to auto-detect a template's variables and seed default values for an in-browser preview, instead of requiring the variables to be registered by hand.
|
|
20
|
+
|
|
21
|
+
## What it detects
|
|
22
|
+
|
|
23
|
+
- `tval` / `tbval` / `tival` / `tfval` and `useTemplateVal` / `useTemplateBoolVal` / `useTemplateIntVal` / `useTemplateFloatVal` calls, imported from `@dsplay/template-utils` / `@dsplay/react-template-utils` **or** called off the `dsplayTemplateUtils` global UMD alias (`u.tval(...)`) used by bundler-less templates.
|
|
24
|
+
- Direct property access or destructuring off the template object, with no helper call at all — `template.title`, `useTemplate().title`, `const { title } = template`, `u.template.title` — including type promotion when wrapped in `parseInt`/`parseFloat`.
|
|
25
|
+
- A best-effort `default` value, resolved through local consts and chained `tval(...)`-as-default calls using each reference's own lexical scope (never a global cross-file lookup, since two files can legitimately use the same key with different local defaults). Defaults that are computed at runtime (e.g. `list.join(',')`) are left unresolved rather than guessed.
|
|
26
|
+
- A `subtypeGuess` (`image`, `video`, `color`) from the variable's key name, for `string`-typed variables — always a suggestion, never authoritative.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install --save-dev @dsplay/template-manifest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Vite plugin
|
|
37
|
+
|
|
38
|
+
For templates built with Vite:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// vite.config.js
|
|
42
|
+
import templateManifest from '@dsplay/template-manifest/vite-plugin';
|
|
43
|
+
|
|
44
|
+
export default defineConfig({
|
|
45
|
+
plugins: [
|
|
46
|
+
// ...
|
|
47
|
+
templateManifest(),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It writes `template-variables.json` and `template-example-data.json` into the build output directory on `closeBundle`, so they end up inside `template.zip` alongside `index.html` and the rest of the build.
|
|
53
|
+
|
|
54
|
+
By default it scans `src` and reads `public/dsplay-data.js` (both resolved relative to the Vite root). Override with:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
templateManifest({ srcDir: 'source', dsplayDataPath: 'public/mock-data.js' });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### CLI
|
|
61
|
+
|
|
62
|
+
For templates with no bundler (e.g. the vanilla-js and jQuery boilerplates) or on an older build stack, run the `dsplay-scan-template` CLI from your packaging script (e.g. `pack.sh`):
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx dsplay-scan-template --src scripts --dsplay-data scripts/dsplay-data.js --out .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Flag | Default | Description |
|
|
69
|
+
|------------------|--------------------------|-----------------------------------------------|
|
|
70
|
+
| `--src` | `src` | Directory to scan recursively for `.js`/`.jsx` files |
|
|
71
|
+
| `--dsplay-data` | `public/dsplay-data.js` | Path to the template's `dsplay-data.js` mock |
|
|
72
|
+
| `--out` | `build` | Directory to write the two JSON files into |
|
|
73
|
+
|
|
74
|
+
### Programmatic API
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { scanTemplateVariables, scanExampleData, generateManifest } from '@dsplay/template-manifest';
|
|
78
|
+
|
|
79
|
+
const variables = scanTemplateVariables('src'); // [{key, type, default?, subtypeGuess?}]
|
|
80
|
+
const example = scanExampleData('public/dsplay-data.js'); // {config?, media?, template?}
|
|
81
|
+
|
|
82
|
+
generateManifest({ srcDir: 'src', dsplayDataPath: 'public/dsplay-data.js', outDir: 'build' });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Why a separate package
|
|
86
|
+
|
|
87
|
+
This scanner depends on `@babel/parser`/`@babel/traverse`, which are only needed at build time by templates that opt into this tool. Bundling it inside `@dsplay/template-utils` would have forced those dependencies onto every consumer of that package — including every React template via `@dsplay/react-template-utils` — even though the vast majority never use the scanner.
|
package/cli.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { generateManifest } from './core.js';
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const options = { src: 'src', dsplayData: 'public/dsplay-data.js', out: 'build' };
|
|
7
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
8
|
+
const arg = argv[i];
|
|
9
|
+
if (arg === '--src') options.src = argv[(i += 1)];
|
|
10
|
+
else if (arg === '--dsplay-data') options.dsplayData = argv[(i += 1)];
|
|
11
|
+
else if (arg === '--out') options.out = argv[(i += 1)];
|
|
12
|
+
else {
|
|
13
|
+
process.stderr.write(`Unknown argument: ${arg}\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { src, dsplayData, out } = parseArgs(process.argv.slice(2));
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
|
|
23
|
+
const { variables } = generateManifest({
|
|
24
|
+
srcDir: path.resolve(cwd, src),
|
|
25
|
+
dsplayDataPath: path.resolve(cwd, dsplayData),
|
|
26
|
+
outDir: path.resolve(cwd, out),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
process.stdout.write(`dsplay-scan-template: found ${variables.length} template variable(s), wrote manifest to ${path.resolve(cwd, out)}\n`);
|
package/core.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import traverseModule from '@babel/traverse';
|
|
5
|
+
|
|
6
|
+
const traverse = traverseModule.default || traverseModule;
|
|
7
|
+
|
|
8
|
+
const HOOK_TYPES = {
|
|
9
|
+
tval: 'string',
|
|
10
|
+
useTemplateVal: 'string',
|
|
11
|
+
tbval: 'boolean',
|
|
12
|
+
useTemplateBoolVal: 'boolean',
|
|
13
|
+
tival: 'integer',
|
|
14
|
+
useTemplateIntVal: 'integer',
|
|
15
|
+
tfval: 'float',
|
|
16
|
+
useTemplateFloatVal: 'float',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// The global UMD build of @dsplay/template-utils exposes itself under this fixed name
|
|
20
|
+
// (see webpack.config.js's output.library) for templates with no bundler/import system at
|
|
21
|
+
// all (plain <script> tags, e.g. the vanilla-js and jQuery boilerplates).
|
|
22
|
+
const GLOBAL_UTILS_NAME = 'dsplayTemplateUtils';
|
|
23
|
+
|
|
24
|
+
const PACKAGE_NAMES = ['@dsplay/template-utils', '@dsplay/react-template-utils'];
|
|
25
|
+
|
|
26
|
+
const SOURCE_EXTENSIONS = ['.js', '.jsx'];
|
|
27
|
+
|
|
28
|
+
const IMAGE_NAME_HINT = /logo|image|background|bg[_-]|[_-]bg|icon|photo|picture|banner|thumbnail/i;
|
|
29
|
+
const VIDEO_NAME_HINT = /video/i;
|
|
30
|
+
const COLOR_NAME_HINT = /colou?r/i;
|
|
31
|
+
|
|
32
|
+
function guessSubtype(key, type) {
|
|
33
|
+
if (type !== 'string') return undefined;
|
|
34
|
+
if (VIDEO_NAME_HINT.test(key)) return 'video';
|
|
35
|
+
if (IMAGE_NAME_HINT.test(key)) return 'image';
|
|
36
|
+
if (COLOR_NAME_HINT.test(key)) return 'color';
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function walkSourceFiles(dir, out = []) {
|
|
41
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
42
|
+
if (entry.name === 'node_modules') continue;
|
|
43
|
+
const full = path.join(dir, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
walkSourceFiles(full, out);
|
|
46
|
+
} else if (SOURCE_EXTENSIONS.includes(path.extname(entry.name))) {
|
|
47
|
+
out.push(full);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseFile(code) {
|
|
54
|
+
return parse(code, {
|
|
55
|
+
sourceType: 'unambiguous',
|
|
56
|
+
plugins: ['jsx'],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Evaluates a "data literal" AST node (string/number/boolean/null/array/object of the same)
|
|
61
|
+
// into a plain JS value. Anything referencing identifiers/runtime globals is left undefined.
|
|
62
|
+
function evalLiteral(node) {
|
|
63
|
+
if (!node) return undefined;
|
|
64
|
+
switch (node.type) {
|
|
65
|
+
case 'StringLiteral':
|
|
66
|
+
case 'NumericLiteral':
|
|
67
|
+
case 'BooleanLiteral':
|
|
68
|
+
return node.value;
|
|
69
|
+
case 'NullLiteral':
|
|
70
|
+
return null;
|
|
71
|
+
case 'UnaryExpression':
|
|
72
|
+
if (node.operator === '-' && node.argument.type === 'NumericLiteral') {
|
|
73
|
+
return -node.argument.value;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
case 'ArrayExpression': {
|
|
77
|
+
const arr = [];
|
|
78
|
+
for (const el of node.elements) {
|
|
79
|
+
if (el == null) { arr.push(null); continue; }
|
|
80
|
+
const v = evalLiteral(el);
|
|
81
|
+
if (v === undefined && el.type !== 'NullLiteral') return undefined;
|
|
82
|
+
arr.push(v);
|
|
83
|
+
}
|
|
84
|
+
return arr;
|
|
85
|
+
}
|
|
86
|
+
case 'ObjectExpression': {
|
|
87
|
+
const obj = {};
|
|
88
|
+
for (const prop of node.properties) {
|
|
89
|
+
if (prop.type !== 'ObjectProperty' || prop.computed) continue;
|
|
90
|
+
const propKey = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value;
|
|
91
|
+
const v = evalLiteral(prop.value);
|
|
92
|
+
if (v === undefined && prop.value.type !== 'NullLiteral') continue;
|
|
93
|
+
obj[propKey] = v;
|
|
94
|
+
}
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function literalType(value) {
|
|
103
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
104
|
+
if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'float';
|
|
105
|
+
if (typeof value === 'string') return 'string';
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// True when `node` is an Identifier that (directly, or via one or more local `var x = ...`
|
|
110
|
+
// indirections) refers to the dsplayTemplateUtils UMD global - or `window.dsplayTemplateUtils`.
|
|
111
|
+
function resolvesToGlobalUtilsAlias(node, scope, seen = new Set()) {
|
|
112
|
+
if (!node) return false;
|
|
113
|
+
if (node.type === 'Identifier') {
|
|
114
|
+
if (node.name === GLOBAL_UTILS_NAME) return true;
|
|
115
|
+
if (seen.has(node.name)) return false;
|
|
116
|
+
const binding = scope.getBinding(node.name);
|
|
117
|
+
const init = binding?.path?.isVariableDeclarator?.() ? binding.path.node.init : undefined;
|
|
118
|
+
return init ? resolvesToGlobalUtilsAlias(init, scope, new Set(seen).add(node.name)) : false;
|
|
119
|
+
}
|
|
120
|
+
if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier') {
|
|
121
|
+
return node.property.name === GLOBAL_UTILS_NAME;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Returns the tval/tbval/tival/tfval "kind" a call's callee resolves to, whether reached via
|
|
127
|
+
// an ES import binding (`tval(...)`) or a member access off the global UMD alias (`u.tval(...)`).
|
|
128
|
+
function getHookType(callee, scope) {
|
|
129
|
+
if (callee.type === 'Identifier') {
|
|
130
|
+
const binding = scope.getBinding(callee.name);
|
|
131
|
+
const importedName = binding?.path?.isImportSpecifier?.() ? binding.path.node.imported.name : undefined;
|
|
132
|
+
return importedName ? HOOK_TYPES[importedName] : undefined;
|
|
133
|
+
}
|
|
134
|
+
if (callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier') {
|
|
135
|
+
if (!resolvesToGlobalUtilsAlias(callee.object, scope)) return undefined;
|
|
136
|
+
return HOOK_TYPES[callee.property.name];
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// True when `node` is an expression representing "the template variables object" itself -
|
|
142
|
+
// covers the ES default export, the React `useTemplate()` hook result, and the global UMD
|
|
143
|
+
// alias's `.template` property (plus one level of local variable indirection for each).
|
|
144
|
+
function isTemplateAccessBase(node, scope, seen = new Set()) {
|
|
145
|
+
if (!node) return false;
|
|
146
|
+
|
|
147
|
+
if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier'
|
|
148
|
+
&& node.property.name === 'template') {
|
|
149
|
+
return resolvesToGlobalUtilsAlias(node.object, scope);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
|
|
153
|
+
const binding = scope.getBinding(node.callee.name);
|
|
154
|
+
const importedName = binding?.path?.isImportSpecifier?.() ? binding.path.node.imported.name : undefined;
|
|
155
|
+
return importedName === 'useTemplate';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (node.type === 'Identifier') {
|
|
159
|
+
if (seen.has(node.name)) return false;
|
|
160
|
+
const binding = scope.getBinding(node.name);
|
|
161
|
+
if (!binding) return false;
|
|
162
|
+
if (binding.path.isImportDefaultSpecifier?.()) {
|
|
163
|
+
const importDecl = binding.path.parentPath;
|
|
164
|
+
return PACKAGE_NAMES.includes(importDecl.node.source.value);
|
|
165
|
+
}
|
|
166
|
+
const init = binding.path.isVariableDeclarator?.() ? binding.path.node.init : undefined;
|
|
167
|
+
return init ? isTemplateAccessBase(init, scope, new Set(seen).add(node.name)) : false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Descriptor for a var's default arg, resolved as far as static analysis allows by walking
|
|
174
|
+
// straight through the AST (local const references, chained tval(...)-as-default calls) using
|
|
175
|
+
// each node's own lexical scope — never through a global cross-file key lookup, since two
|
|
176
|
+
// different call sites can legitimately use the same key with different local defaults.
|
|
177
|
+
function describeDefault(node, scope, seen = new Set()) {
|
|
178
|
+
if (!node) return { kind: 'none' };
|
|
179
|
+
|
|
180
|
+
const literalValue = evalLiteral(node);
|
|
181
|
+
if (literalValue !== undefined || node.type === 'NullLiteral') {
|
|
182
|
+
return { kind: 'literal', value: literalValue };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (node.type === 'CallExpression' && getHookType(node.callee, scope)) {
|
|
186
|
+
// e.g. useTemplateVal('outer', useTemplateVal('inner')) - the outer's default is
|
|
187
|
+
// whatever the inner reference itself resolves to as ITS OWN default.
|
|
188
|
+
return describeDefault(node.arguments[1], scope, seen);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (node.type === 'Identifier') {
|
|
192
|
+
if (seen.has(node.name)) return { kind: 'unresolved' };
|
|
193
|
+
const binding = scope.getBinding(node.name);
|
|
194
|
+
const init = binding?.path?.isVariableDeclarator?.() ? binding.path.node.init : undefined;
|
|
195
|
+
if (init) return describeDefault(init, scope, new Set(seen).add(node.name));
|
|
196
|
+
return { kind: 'unresolved' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { kind: 'unresolved' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function addVariable(table, key, type, { default: defaultValue, hasDefault = false } = {}) {
|
|
203
|
+
const existing = table.get(key);
|
|
204
|
+
if (existing) return existing;
|
|
205
|
+
|
|
206
|
+
const variable = { key, type };
|
|
207
|
+
if (hasDefault) variable.default = defaultValue;
|
|
208
|
+
const subtype = guessSubtype(key, type);
|
|
209
|
+
if (subtype) variable.subtypeGuess = subtype;
|
|
210
|
+
table.set(key, variable);
|
|
211
|
+
return variable;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function scanFile(code, table) {
|
|
215
|
+
let ast;
|
|
216
|
+
try {
|
|
217
|
+
ast = parseFile(code);
|
|
218
|
+
} catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
traverse(ast, {
|
|
223
|
+
// tval('key', default) / useTemplateVal('key', default) / u.tval('key', default)
|
|
224
|
+
CallExpression(nodePath) {
|
|
225
|
+
const { callee, arguments: args } = nodePath.node;
|
|
226
|
+
const type = getHookType(callee, nodePath.scope);
|
|
227
|
+
if (!type || args[0]?.type !== 'StringLiteral') return;
|
|
228
|
+
|
|
229
|
+
const key = args[0].value;
|
|
230
|
+
const defaultDescriptor = describeDefault(args[1], nodePath.scope);
|
|
231
|
+
addVariable(table, key, type, {
|
|
232
|
+
default: defaultDescriptor.kind === 'literal' ? defaultDescriptor.value : undefined,
|
|
233
|
+
hasDefault: defaultDescriptor.kind === 'literal',
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// template.key / useTemplate().key / u.template.key (direct read, no helper call)
|
|
238
|
+
MemberExpression(nodePath) {
|
|
239
|
+
const node = nodePath.node;
|
|
240
|
+
if (node.computed || node.property.type !== 'Identifier') return;
|
|
241
|
+
if (!isTemplateAccessBase(node.object, nodePath.scope)) return;
|
|
242
|
+
|
|
243
|
+
const key = node.property.name;
|
|
244
|
+
let type = 'string';
|
|
245
|
+
const parent = nodePath.parentPath;
|
|
246
|
+
if (parent?.isCallExpression() && parent.node.arguments[0] === node
|
|
247
|
+
&& parent.node.callee.type === 'Identifier') {
|
|
248
|
+
if (parent.node.callee.name === 'parseInt') type = 'integer';
|
|
249
|
+
else if (parent.node.callee.name === 'parseFloat') type = 'float';
|
|
250
|
+
}
|
|
251
|
+
addVariable(table, key, type);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// const { key, other = 'default' } = template / useTemplate() / u.template
|
|
255
|
+
VariableDeclarator(nodePath) {
|
|
256
|
+
const node = nodePath.node;
|
|
257
|
+
if (node.id.type !== 'ObjectPattern' || !isTemplateAccessBase(node.init, nodePath.scope)) return;
|
|
258
|
+
|
|
259
|
+
for (const prop of node.id.properties) {
|
|
260
|
+
if (prop.type !== 'ObjectProperty' || prop.computed) continue;
|
|
261
|
+
const key = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value;
|
|
262
|
+
|
|
263
|
+
if (prop.value.type === 'AssignmentPattern') {
|
|
264
|
+
const literalValue = evalLiteral(prop.value.right);
|
|
265
|
+
const type = literalType(literalValue) || 'string';
|
|
266
|
+
addVariable(table, key, type, { default: literalValue, hasDefault: literalValue !== undefined });
|
|
267
|
+
} else {
|
|
268
|
+
addVariable(table, key, 'string');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Statically scans a template's source tree for DSPLAY template-variable reads
|
|
277
|
+
* (`tval`/`useTemplateVal`-style calls, `useTemplate()`/`.template` destructuring or direct
|
|
278
|
+
* property access, in both the ES-import and global-UMD-alias worlds) and returns the
|
|
279
|
+
* discovered `{key, type, default?, subtypeGuess?}` entries.
|
|
280
|
+
* @param {string} srcDir - Directory to scan recursively for .js/.jsx files.
|
|
281
|
+
* @returns {Array<{key: string, type: 'string'|'boolean'|'integer'|'float', default?: *, subtypeGuess?: string}>}
|
|
282
|
+
*/
|
|
283
|
+
export function scanTemplateVariables(srcDir) {
|
|
284
|
+
const table = new Map();
|
|
285
|
+
for (const file of walkSourceFiles(srcDir)) {
|
|
286
|
+
scanFile(readFileSync(file, 'utf-8'), table);
|
|
287
|
+
}
|
|
288
|
+
return [...table.values()];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function extractDsplayDataGlobal(ast, globalName) {
|
|
292
|
+
let result;
|
|
293
|
+
traverse(ast, {
|
|
294
|
+
VariableDeclarator(nodePath) {
|
|
295
|
+
if (nodePath.node.id.type === 'Identifier' && nodePath.node.id.name === globalName) {
|
|
296
|
+
result = evalLiteral(nodePath.node.init);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Captures a template's `dsplay-data.js` mock (`dsplay_config`/`dsplay_media`/`dsplay_template`)
|
|
305
|
+
* as plain example data. Fields that aren't statically-evaluable literals (e.g. an `orientation`
|
|
306
|
+
* computed from `window.innerWidth`) are silently dropped rather than poisoning the whole object.
|
|
307
|
+
* @param {string} dsplayDataPath - Path to the template's dsplay-data.js file.
|
|
308
|
+
* @returns {{config?: object, media?: object, template?: object}}
|
|
309
|
+
*/
|
|
310
|
+
export function scanExampleData(dsplayDataPath) {
|
|
311
|
+
if (!existsSync(dsplayDataPath)) return {};
|
|
312
|
+
|
|
313
|
+
const code = readFileSync(dsplayDataPath, 'utf-8');
|
|
314
|
+
let ast;
|
|
315
|
+
try {
|
|
316
|
+
ast = parse(code, { sourceType: 'script' });
|
|
317
|
+
} catch {
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const example = {};
|
|
322
|
+
const config = extractDsplayDataGlobal(ast, 'dsplay_config');
|
|
323
|
+
const media = extractDsplayDataGlobal(ast, 'dsplay_media');
|
|
324
|
+
const template = extractDsplayDataGlobal(ast, 'dsplay_template');
|
|
325
|
+
if (config) example.config = config;
|
|
326
|
+
if (media) example.media = media;
|
|
327
|
+
if (template) example.template = template;
|
|
328
|
+
return example;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Runs both scans and writes `template-variables.json` + `template-example-data.json` into
|
|
333
|
+
* `outDir` (creating it if needed) - the pair of files a template's build should ship inside
|
|
334
|
+
* its `template.zip` so the CMS can auto-detect variables and seed default preview values.
|
|
335
|
+
* @param {{srcDir: string, dsplayDataPath: string, outDir: string}} options
|
|
336
|
+
*/
|
|
337
|
+
export function generateManifest({ srcDir, dsplayDataPath, outDir }) {
|
|
338
|
+
const variables = scanTemplateVariables(srcDir);
|
|
339
|
+
const example = scanExampleData(dsplayDataPath);
|
|
340
|
+
|
|
341
|
+
mkdirSync(outDir, { recursive: true });
|
|
342
|
+
writeFileSync(
|
|
343
|
+
path.join(outDir, 'template-variables.json'),
|
|
344
|
+
`${JSON.stringify({ variables }, null, 2)}\n`,
|
|
345
|
+
);
|
|
346
|
+
writeFileSync(
|
|
347
|
+
path.join(outDir, 'template-example-data.json'),
|
|
348
|
+
`${JSON.stringify(example, null, 2)}\n`,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return { variables, example };
|
|
352
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsplay/template-manifest",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Statically scans a DSPLAY HTML template's source for its dsplay_template variables and captures its dsplay-data.js as example preview data, producing template-variables.json + template-example-data.json for the CMS to auto-detect variables and seed default preview values.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./core.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./core.js",
|
|
9
|
+
"./vite-plugin": "./vite-plugin.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"dsplay-scan-template": "cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"core.js",
|
|
17
|
+
"vite-plugin.js",
|
|
18
|
+
"cli.js"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@babel/parser": "^7.29.7",
|
|
26
|
+
"@babel/traverse": "^7.29.7"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/vite-plugin.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { generateManifest } from './core.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vite plugin that generates `template-variables.json` + `template-example-data.json` into
|
|
6
|
+
* the build output directory once the build finishes - see `generateManifest` for details.
|
|
7
|
+
* @param {{srcDir?: string, dsplayDataPath?: string}} [options] - Paths relative to the
|
|
8
|
+
* resolved Vite root. Defaults match this ecosystem's convention: `src` and `public/dsplay-data.js`.
|
|
9
|
+
*/
|
|
10
|
+
export default function templateManifestPlugin(options = {}) {
|
|
11
|
+
const { srcDir = 'src', dsplayDataPath = 'public/dsplay-data.js' } = options;
|
|
12
|
+
let config;
|
|
13
|
+
let done = false;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: 'dsplay-template-manifest',
|
|
17
|
+
configResolved(resolvedConfig) {
|
|
18
|
+
config = resolvedConfig;
|
|
19
|
+
},
|
|
20
|
+
closeBundle() {
|
|
21
|
+
if (done) return;
|
|
22
|
+
done = true;
|
|
23
|
+
|
|
24
|
+
generateManifest({
|
|
25
|
+
srcDir: path.resolve(config.root, srcDir),
|
|
26
|
+
dsplayDataPath: path.resolve(config.root, dsplayDataPath),
|
|
27
|
+
outDir: path.resolve(config.root, config.build.outDir),
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|