@newlogic-digital/cli 1.5.0-next.4 → 1.5.0-next.6
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/index.mjs +6 -52
- package/package.json +1 -1
- package/skills/newlogic-cli/SKILL.md +23 -5
- package/src/cli/args.mjs +49 -0
- package/src/commands/blocks/index.mjs +7 -3
- package/src/commands/blocks/service.mjs +227 -56
- package/src/templates/components/article/ArticleGridHorizontal.latte +0 -28
- package/src/templates/components/article/ArticleItem.latte +0 -59
- package/src/templates/components/header/HeaderNavLeft.latte +0 -71
package/index.mjs
CHANGED
|
@@ -4,15 +4,12 @@ import init from './src/commands/init/index.mjs'
|
|
|
4
4
|
import cms from './src/commands/cms/index.mjs'
|
|
5
5
|
import blocks from './src/commands/blocks/index.mjs'
|
|
6
6
|
import skills from './src/commands/skills/index.mjs'
|
|
7
|
+
import { parseCommandArgs } from './src/cli/args.mjs'
|
|
7
8
|
import { styleText } from 'node:util'
|
|
8
9
|
import { version, name } from './src/utils.mjs'
|
|
9
10
|
|
|
10
11
|
const knownCommands = ['init', 'cms', 'blocks', 'skills']
|
|
11
12
|
|
|
12
|
-
function normalizeOptionName(name) {
|
|
13
|
-
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
14
|
-
}
|
|
15
|
-
|
|
16
13
|
function getLevenshteinDistance(left, right) {
|
|
17
14
|
const distances = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0))
|
|
18
15
|
|
|
@@ -73,51 +70,6 @@ function printUnknownCommand(command) {
|
|
|
73
70
|
console.log(lines.join('\n'))
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
function parseCommandArgs(args) {
|
|
77
|
-
const positionals = []
|
|
78
|
-
const options = {}
|
|
79
|
-
|
|
80
|
-
for (let i = 0; i < args.length; i++) {
|
|
81
|
-
const arg = args[i]
|
|
82
|
-
|
|
83
|
-
if (arg === '-y') {
|
|
84
|
-
options.y = true
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!arg.startsWith('--')) {
|
|
89
|
-
positionals.push(arg)
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (arg.startsWith('--no-')) {
|
|
94
|
-
options[normalizeOptionName(arg.slice(5))] = false
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (arg.includes('=')) {
|
|
99
|
-
const splitIndex = arg.indexOf('=')
|
|
100
|
-
const key = normalizeOptionName(arg.slice(2, splitIndex))
|
|
101
|
-
const value = arg.slice(splitIndex + 1)
|
|
102
|
-
|
|
103
|
-
options[key] = value || true
|
|
104
|
-
continue
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const key = normalizeOptionName(arg.slice(2))
|
|
108
|
-
const nextArg = args[i + 1]
|
|
109
|
-
|
|
110
|
-
if (nextArg && !nextArg.startsWith('-')) {
|
|
111
|
-
options[key] = nextArg
|
|
112
|
-
i++
|
|
113
|
-
continue
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
options[key] = true
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { positionals, options }
|
|
120
|
-
}
|
|
121
73
|
|
|
122
74
|
const rawArgs = process.argv.slice(2)
|
|
123
75
|
const command = rawArgs[0]
|
|
@@ -150,11 +102,13 @@ if (!command) {
|
|
|
150
102
|
|
|
151
103
|
-- blocks --
|
|
152
104
|
${styleText('green', 'newlogic blocks list')} - Lists all available installable blocks with descriptions
|
|
153
|
-
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name...>')} - Installs one or more blocks by kebab-case or PascalCase name
|
|
105
|
+
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name[@variant]...>')} ${styleText('cyan', '[--target=<ui|cms>]')} - Installs one or more blocks by kebab-case or PascalCase name
|
|
154
106
|
${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', '<name...>')} - Removes one or more blocks and orphaned dependencies
|
|
155
107
|
${styleText('green', 'newlogic blocks update')} - Reinstalls all configured blocks from ${styleText('yellow', 'newlogic.config.json')}
|
|
156
108
|
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'about-accordion')}
|
|
157
109
|
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'AboutAccordion')}
|
|
110
|
+
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left@stimulus')}
|
|
111
|
+
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left contact-info-card')} ${styleText('cyan', '--target=ui')}
|
|
158
112
|
${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left contact-info-card hero-floating-text')}
|
|
159
113
|
${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', 'about-accordion')}
|
|
160
114
|
${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', 'header-nav-left hero-floating-text')}
|
|
@@ -184,11 +138,11 @@ if (command === 'cms') {
|
|
|
184
138
|
}
|
|
185
139
|
|
|
186
140
|
if (command === 'blocks') {
|
|
187
|
-
const { positionals } = parseCommandArgs(rawArgs.slice(1))
|
|
141
|
+
const { positionals, options } = parseCommandArgs(rawArgs.slice(1))
|
|
188
142
|
const action = positionals[0]
|
|
189
143
|
const names = positionals.slice(1)
|
|
190
144
|
|
|
191
|
-
await blocks(action, names)
|
|
145
|
+
await blocks(action, names, options)
|
|
192
146
|
}
|
|
193
147
|
|
|
194
148
|
if (command === 'skills') {
|
package/package.json
CHANGED
|
@@ -60,23 +60,41 @@ Notes:
|
|
|
60
60
|
Use to manage installable blocks in a project root.
|
|
61
61
|
|
|
62
62
|
- `newlogic blocks list`
|
|
63
|
-
- `newlogic blocks add <name
|
|
63
|
+
- `newlogic blocks add <name[@variant]...> [--target=<ui|cms>]`
|
|
64
64
|
- `newlogic blocks remove <name...>`
|
|
65
65
|
- `newlogic blocks update`
|
|
66
66
|
|
|
67
67
|
Notes:
|
|
68
68
|
|
|
69
69
|
- `list` prints all installable blocks with short descriptions so the agent can discover valid names before making changes.
|
|
70
|
-
- `add` accepts kebab-case or PascalCase block names
|
|
71
|
-
-
|
|
72
|
-
- `
|
|
70
|
+
- `add` accepts kebab-case or PascalCase block names and optional `@variant`.
|
|
71
|
+
- `--target` is a batch option for the whole `add` command. `newlogic blocks add blok-1@stimulus blok-2 --target=ui` applies `target: "ui"` to both root blocks.
|
|
72
|
+
- Use `--target=ui` when you want to copy only the frontend block files into a project where the CMS part does not exist yet.
|
|
73
|
+
- Use `--target=cms` when the project already has the UI frontend and UI block files, but is still missing the backend/CMS implementation for those blocks.
|
|
74
|
+
- Installed blocks are recorded in `newlogic.config.json` as an object map keyed by block name.
|
|
75
|
+
- `update` reinstalls all configured blocks from `newlogic.config.json`, including stored `variant` and `target`.
|
|
76
|
+
- When a block metadata file rule defines `target`, only matching files are installed for that block config. Without `target`, all files are installed.
|
|
73
77
|
- `add` and `remove` may modify files and install npm or composer dependencies.
|
|
74
78
|
|
|
79
|
+
Current config format:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"blocks": {
|
|
84
|
+
"about-horizontal": {
|
|
85
|
+
"variant": "stimulus",
|
|
86
|
+
"target": "ui"
|
|
87
|
+
},
|
|
88
|
+
"hero-floating-text": {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
75
93
|
Recommended flow:
|
|
76
94
|
|
|
77
95
|
1. Run `newlogic blocks list` to discover what blocks exist.
|
|
78
96
|
2. Match the user request to one or more listed block names.
|
|
79
|
-
3. Run `newlogic blocks add <name...>` only after the names are confirmed by the list output.
|
|
97
|
+
3. Run `newlogic blocks add <name[@variant]...>` only after the names are confirmed by the list output.
|
|
80
98
|
4. Use `remove` only when the user wants to delete blocks that are already installed in the current project.
|
|
81
99
|
5. Use `update` when the goal is to reinstall everything already tracked in `newlogic.config.json`.
|
|
82
100
|
|
package/src/cli/args.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function normalizeOptionName(name) {
|
|
2
|
+
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function parseCommandArgs(args) {
|
|
6
|
+
const positionals = []
|
|
7
|
+
const options = {}
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i]
|
|
11
|
+
|
|
12
|
+
if (arg === '-y') {
|
|
13
|
+
options.y = true
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!arg.startsWith('--')) {
|
|
18
|
+
positionals.push(arg)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (arg.startsWith('--no-')) {
|
|
23
|
+
options[normalizeOptionName(arg.slice(5))] = false
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (arg.includes('=')) {
|
|
28
|
+
const splitIndex = arg.indexOf('=')
|
|
29
|
+
const key = normalizeOptionName(arg.slice(2, splitIndex))
|
|
30
|
+
const value = arg.slice(splitIndex + 1)
|
|
31
|
+
|
|
32
|
+
options[key] = value || true
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const key = normalizeOptionName(arg.slice(2))
|
|
37
|
+
const nextArg = args[i + 1]
|
|
38
|
+
|
|
39
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
40
|
+
options[key] = nextArg
|
|
41
|
+
i++
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
options[key] = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { positionals, options }
|
|
49
|
+
}
|
|
@@ -134,18 +134,20 @@ function printBlocksUsage() {
|
|
|
134
134
|
styleText(['white', 'bold'], 'Usage:'),
|
|
135
135
|
'',
|
|
136
136
|
` ${styleText('green', 'newlogic blocks list')} - Lists all available blocks with descriptions`,
|
|
137
|
-
` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name...>')} - Adds one or more blocks by kebab-case or PascalCase name`,
|
|
137
|
+
` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name[@variant]...>')} ${styleText('cyan', '[--target=<ui|cms>]')} - Adds one or more blocks by kebab-case or PascalCase name`,
|
|
138
138
|
` ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', '<name...>')} - Removes one or more blocks and orphaned dependencies`,
|
|
139
139
|
` ${styleText('green', 'newlogic blocks update')} - Reinstalls all configured blocks from ${styleText('yellow', 'newlogic.config.json')}`,
|
|
140
140
|
'',
|
|
141
141
|
styleText(['white', 'bold'], 'Examples:'),
|
|
142
142
|
'',
|
|
143
143
|
` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left contact-info-card hero-floating-text')}`,
|
|
144
|
+
` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left@stimulus')}`,
|
|
145
|
+
` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left contact-info-card')} ${styleText('cyan', '--target=ui')}`,
|
|
144
146
|
` ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', 'header-nav-left hero-floating-text')}`,
|
|
145
147
|
].join('\n'))
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
export default async function blocks(action, names = []) {
|
|
150
|
+
export default async function blocks(action, names = [], options = {}) {
|
|
149
151
|
if (!action || action === 'help' || action === '--help') {
|
|
150
152
|
printBlocksUsage()
|
|
151
153
|
return
|
|
@@ -166,7 +168,9 @@ export default async function blocks(action, names = []) {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
const service = createBlocksService({ logger: createCliLogger() })
|
|
169
|
-
await service.addBlocks(names
|
|
171
|
+
await service.addBlocks(names, {
|
|
172
|
+
target: options.target,
|
|
173
|
+
})
|
|
170
174
|
return
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -8,6 +8,7 @@ import { createRemoteBlocksRepository } from './repository.mjs'
|
|
|
8
8
|
|
|
9
9
|
const CONFIG_FILE_NAME = 'newlogic.config.json'
|
|
10
10
|
const BLOCK_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
11
|
+
const BLOCK_TARGETS = new Set(['ui', 'cms'])
|
|
11
12
|
|
|
12
13
|
function defaultLogger() {
|
|
13
14
|
return {
|
|
@@ -83,10 +84,102 @@ function assertBlockName(name, label = 'block name') {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
function normalizeBlockTarget(rawValue, label = 'block target') {
|
|
88
|
+
if (rawValue == null) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const target = `${rawValue}`.trim().toLowerCase()
|
|
93
|
+
|
|
94
|
+
if (!target) {
|
|
95
|
+
throw new Error(`Invalid ${label}: "${rawValue}"`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!BLOCK_TARGETS.has(target)) {
|
|
99
|
+
throw new Error(`Invalid ${label}: "${target}"`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return target
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseBlockSpecifier(rawValue, {
|
|
106
|
+
label = 'block name',
|
|
107
|
+
} = {}) {
|
|
108
|
+
const value = `${rawValue ?? ''}`.trim()
|
|
109
|
+
|
|
110
|
+
if (!value) {
|
|
111
|
+
throw new Error(`Missing ${label}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const variantSeparator = value.indexOf('@')
|
|
115
|
+
const rawName = variantSeparator === -1 ? value : value.slice(0, variantSeparator)
|
|
116
|
+
const rawVariant = variantSeparator === -1 ? '' : value.slice(variantSeparator + 1).trim()
|
|
117
|
+
const name = normalizeBlockName(rawName)
|
|
118
|
+
|
|
119
|
+
assertBlockName(name, label)
|
|
120
|
+
|
|
121
|
+
if (variantSeparator !== -1 && !rawVariant) {
|
|
122
|
+
throw new Error(`Invalid ${label}: "${value}"`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
variant: rawVariant || undefined,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
86
131
|
function toBlockKey(block) {
|
|
87
132
|
return `${block.name}@${block.variant}`
|
|
88
133
|
}
|
|
89
134
|
|
|
135
|
+
function createTraversalKey(name, variant, target) {
|
|
136
|
+
return `${name}@${variant}#${target || '*'}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createConfiguredBlock(name, config = {}) {
|
|
140
|
+
const nextConfig = { ...config }
|
|
141
|
+
|
|
142
|
+
if (nextConfig.variant != null) {
|
|
143
|
+
const variant = `${nextConfig.variant}`.trim()
|
|
144
|
+
|
|
145
|
+
if (!variant) {
|
|
146
|
+
throw new Error(`Configured block "${name}" has an empty variant`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
nextConfig.variant = variant
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (nextConfig.target != null) {
|
|
153
|
+
nextConfig.target = normalizeBlockTarget(nextConfig.target, `configured block "${name}" target`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
variant: nextConfig.variant,
|
|
159
|
+
target: nextConfig.target,
|
|
160
|
+
config: nextConfig,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function filterRulesByTargets(rules = [], targets = []) {
|
|
165
|
+
const targetSet = new Set(targets)
|
|
166
|
+
|
|
167
|
+
if (targetSet.has(undefined)) {
|
|
168
|
+
return rules
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return rules.filter(rule => rule.target && targetSet.has(rule.target))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toManagedFileKey(rule) {
|
|
175
|
+
return [
|
|
176
|
+
rule.source,
|
|
177
|
+
rule.destination,
|
|
178
|
+
rule.parentDirectory || '',
|
|
179
|
+
rule.fileName,
|
|
180
|
+
].join('\0')
|
|
181
|
+
}
|
|
182
|
+
|
|
90
183
|
function resolveTargetPath(projectRoot, rule) {
|
|
91
184
|
const destinationRoot = resolveInside(projectRoot, rule.destination)
|
|
92
185
|
const parentDirectory = rule.parentDirectory ? rule.parentDirectory.split('/').filter(Boolean) : []
|
|
@@ -126,7 +219,7 @@ function loadProjectConfig(projectRoot) {
|
|
|
126
219
|
if (!fs.existsSync(configPath)) {
|
|
127
220
|
return {
|
|
128
221
|
configPath,
|
|
129
|
-
config: { blocks:
|
|
222
|
+
config: { blocks: {} },
|
|
130
223
|
blocks: [],
|
|
131
224
|
}
|
|
132
225
|
}
|
|
@@ -144,42 +237,42 @@ function loadProjectConfig(projectRoot) {
|
|
|
144
237
|
throw new Error(`${CONFIG_FILE_NAME} must contain a JSON object`)
|
|
145
238
|
}
|
|
146
239
|
|
|
147
|
-
const blocksRaw = config.blocks ??
|
|
240
|
+
const blocksRaw = config.blocks ?? {}
|
|
148
241
|
|
|
149
|
-
if (!Array.isArray(blocksRaw)) {
|
|
150
|
-
throw new Error(`"${CONFIG_FILE_NAME}" field "blocks" must be an
|
|
242
|
+
if (!blocksRaw || typeof blocksRaw !== 'object' || Array.isArray(blocksRaw)) {
|
|
243
|
+
throw new Error(`"${CONFIG_FILE_NAME}" field "blocks" must be an object`)
|
|
151
244
|
}
|
|
152
245
|
|
|
153
|
-
const blocks = blocksRaw.map((
|
|
154
|
-
|
|
155
|
-
throw new Error(`Invalid block config entry at index ${index}`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const name = normalizeBlockName(entry.name)
|
|
159
|
-
const variant = `${entry.variant ?? ''}`.trim()
|
|
246
|
+
const blocks = Object.entries(blocksRaw).map(([rawName, rawConfig]) => {
|
|
247
|
+
const name = normalizeBlockName(rawName)
|
|
160
248
|
|
|
161
249
|
assertBlockName(name, 'configured block name')
|
|
162
250
|
|
|
163
|
-
if (!
|
|
164
|
-
throw new Error(`Configured block "${name}"
|
|
251
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
252
|
+
throw new Error(`Configured block "${name}" must use an object value`)
|
|
165
253
|
}
|
|
166
254
|
|
|
167
|
-
return
|
|
255
|
+
return createConfiguredBlock(name, rawConfig)
|
|
168
256
|
})
|
|
169
257
|
|
|
170
|
-
return { configPath, config, blocks
|
|
258
|
+
return { configPath, config, blocks }
|
|
171
259
|
}
|
|
172
260
|
|
|
173
261
|
function saveProjectConfig(configPath, config, blocks) {
|
|
174
262
|
const nextConfig = {
|
|
175
263
|
...config,
|
|
176
|
-
blocks
|
|
264
|
+
blocks: Object.fromEntries(
|
|
265
|
+
blocks.map(block => [
|
|
266
|
+
block.name,
|
|
267
|
+
{ ...(block.config ?? {}) },
|
|
268
|
+
]),
|
|
269
|
+
),
|
|
177
270
|
}
|
|
178
271
|
|
|
179
272
|
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`)
|
|
180
273
|
}
|
|
181
274
|
|
|
182
|
-
function
|
|
275
|
+
function dedupeBlocks(blocks) {
|
|
183
276
|
const byName = new Map()
|
|
184
277
|
|
|
185
278
|
for (const block of blocks) {
|
|
@@ -194,15 +287,41 @@ function normalizeRequestedBlockNames(rawNames, label = 'block name') {
|
|
|
194
287
|
throw new Error(`Missing ${label}`)
|
|
195
288
|
}
|
|
196
289
|
|
|
197
|
-
const normalizedNames = rawNames
|
|
198
|
-
|
|
290
|
+
const normalizedNames = rawNames
|
|
291
|
+
.map(rawName => parseBlockSpecifier(rawName, { label }).name)
|
|
199
292
|
|
|
200
|
-
|
|
293
|
+
return [...new Set(normalizedNames)]
|
|
294
|
+
}
|
|
201
295
|
|
|
202
|
-
|
|
203
|
-
|
|
296
|
+
function normalizeRequestedBlocks(rawNames, {
|
|
297
|
+
label = 'block name',
|
|
298
|
+
target,
|
|
299
|
+
} = {}) {
|
|
300
|
+
if (!Array.isArray(rawNames) || rawNames.length === 0) {
|
|
301
|
+
throw new Error(`Missing ${label}`)
|
|
302
|
+
}
|
|
204
303
|
|
|
205
|
-
|
|
304
|
+
const blocks = rawNames.map(rawName => parseBlockSpecifier(rawName, { label }))
|
|
305
|
+
const byName = new Map()
|
|
306
|
+
|
|
307
|
+
for (const block of blocks) {
|
|
308
|
+
byName.set(block.name, createConfiguredBlock(block.name, {
|
|
309
|
+
...(block.variant ? { variant: block.variant } : {}),
|
|
310
|
+
...(target ? { target } : {}),
|
|
311
|
+
}))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return [...byName.values()]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatConfiguredBlock(block) {
|
|
318
|
+
let value = block.variant ? `${block.name}@${block.variant}` : block.name
|
|
319
|
+
|
|
320
|
+
if (block.target) {
|
|
321
|
+
value = `${value} --target=${block.target}`
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return value
|
|
206
325
|
}
|
|
207
326
|
|
|
208
327
|
async function createMetaValidator(repository) {
|
|
@@ -286,11 +405,13 @@ export function createBlocksService({
|
|
|
286
405
|
}
|
|
287
406
|
|
|
288
407
|
async function resolveInstallPlan(rootBlocks) {
|
|
289
|
-
const
|
|
408
|
+
const orderedKeys = []
|
|
409
|
+
const orderedKeySet = new Set()
|
|
290
410
|
const resolving = new Set()
|
|
291
|
-
const
|
|
411
|
+
const propagated = new Set()
|
|
412
|
+
const itemsByKey = new Map()
|
|
292
413
|
|
|
293
|
-
async function visit(name, preferredVariant) {
|
|
414
|
+
async function visit(name, preferredVariant, target) {
|
|
294
415
|
const normalizedName = normalizeBlockName(name)
|
|
295
416
|
const meta = await getMeta(normalizedName)
|
|
296
417
|
const variantName = preferredVariant || meta.install.defaultVariant
|
|
@@ -301,36 +422,61 @@ export function createBlocksService({
|
|
|
301
422
|
}
|
|
302
423
|
|
|
303
424
|
const key = toBlockKey({ name: normalizedName, variant: variantName })
|
|
425
|
+
const traversalKey = createTraversalKey(normalizedName, variantName, target)
|
|
304
426
|
|
|
305
|
-
if (
|
|
427
|
+
if (propagated.has(traversalKey)) {
|
|
428
|
+
itemsByKey.get(key)?.targets.add(target)
|
|
306
429
|
return
|
|
307
430
|
}
|
|
308
431
|
|
|
309
|
-
if (resolving.has(
|
|
432
|
+
if (resolving.has(traversalKey)) {
|
|
310
433
|
throw new Error(`Dependency cycle detected at "${key}"`)
|
|
311
434
|
}
|
|
312
435
|
|
|
313
|
-
|
|
436
|
+
let item = itemsByKey.get(key)
|
|
437
|
+
|
|
438
|
+
if (!item) {
|
|
439
|
+
item = {
|
|
440
|
+
name: normalizedName,
|
|
441
|
+
variant: variantName,
|
|
442
|
+
meta,
|
|
443
|
+
installVariant: variant,
|
|
444
|
+
targets: new Set(),
|
|
445
|
+
}
|
|
446
|
+
itemsByKey.set(key, item)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
item.targets.add(target)
|
|
450
|
+
resolving.add(traversalKey)
|
|
314
451
|
|
|
315
452
|
for (const dependency of variant.dependencies?.components ?? []) {
|
|
316
|
-
await visit(dependency.name, dependency.variant || variantName)
|
|
453
|
+
await visit(dependency.name, dependency.variant || variantName, target)
|
|
317
454
|
}
|
|
318
455
|
|
|
319
|
-
resolving.delete(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
})
|
|
456
|
+
resolving.delete(traversalKey)
|
|
457
|
+
propagated.add(traversalKey)
|
|
458
|
+
|
|
459
|
+
if (!orderedKeySet.has(key)) {
|
|
460
|
+
orderedKeySet.add(key)
|
|
461
|
+
orderedKeys.push(key)
|
|
462
|
+
}
|
|
327
463
|
}
|
|
328
464
|
|
|
329
465
|
for (const block of rootBlocks) {
|
|
330
|
-
await visit(block.name, block.variant)
|
|
466
|
+
await visit(block.name, block.variant, block.target)
|
|
331
467
|
}
|
|
332
468
|
|
|
333
|
-
return
|
|
469
|
+
return orderedKeys.map((key) => {
|
|
470
|
+
const item = itemsByKey.get(key)
|
|
471
|
+
const targets = [...item.targets]
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
...item,
|
|
475
|
+
targets,
|
|
476
|
+
selectedFiles: filterRulesByTargets(item.installVariant.files ?? [], targets),
|
|
477
|
+
selectedSharedFiles: filterRulesByTargets(item.meta.install.sharedFiles ?? [], targets),
|
|
478
|
+
}
|
|
479
|
+
})
|
|
334
480
|
}
|
|
335
481
|
|
|
336
482
|
function summarizePlan(plan) {
|
|
@@ -340,7 +486,7 @@ export function createBlocksService({
|
|
|
340
486
|
const warnings = []
|
|
341
487
|
|
|
342
488
|
for (const item of plan) {
|
|
343
|
-
const sharedFiles = item.
|
|
489
|
+
const sharedFiles = item.selectedSharedFiles
|
|
344
490
|
|
|
345
491
|
if (sharedFiles.length > 0) {
|
|
346
492
|
warnings.push(`Block "${item.name}" contains ${sharedFiles.length} sharedFiles entries. raw-only mode does not install them yet, skipping.`)
|
|
@@ -419,7 +565,7 @@ export function createBlocksService({
|
|
|
419
565
|
for (const item of plan) {
|
|
420
566
|
logger.info(`install ${item.name}@${item.variant}`)
|
|
421
567
|
|
|
422
|
-
for (const rule of item.
|
|
568
|
+
for (const rule of item.selectedFiles) {
|
|
423
569
|
await installFile(item.name, rule)
|
|
424
570
|
}
|
|
425
571
|
}
|
|
@@ -449,7 +595,7 @@ export function createBlocksService({
|
|
|
449
595
|
for (const item of [...items].reverse()) {
|
|
450
596
|
logger.info(`remove ${item.name}@${item.variant}`)
|
|
451
597
|
|
|
452
|
-
for (const rule of item.
|
|
598
|
+
for (const rule of item.selectedFiles) {
|
|
453
599
|
const targetPath = resolveTargetPath(projectRoot, rule)
|
|
454
600
|
const relativeTargetPath = path.relative(projectRoot, targetPath) || path.basename(targetPath)
|
|
455
601
|
|
|
@@ -477,8 +623,8 @@ export function createBlocksService({
|
|
|
477
623
|
return blocks
|
|
478
624
|
}
|
|
479
625
|
|
|
480
|
-
async function addBlock(rawName) {
|
|
481
|
-
const result = await addBlocks([rawName])
|
|
626
|
+
async function addBlock(rawName, options = {}) {
|
|
627
|
+
const result = await addBlocks([rawName], options)
|
|
482
628
|
|
|
483
629
|
return {
|
|
484
630
|
block: result.blocks[0],
|
|
@@ -487,17 +633,24 @@ export function createBlocksService({
|
|
|
487
633
|
}
|
|
488
634
|
}
|
|
489
635
|
|
|
490
|
-
async function addBlocks(rawNames) {
|
|
491
|
-
const
|
|
636
|
+
async function addBlocks(rawNames, options = {}) {
|
|
637
|
+
const target = normalizeBlockTarget(options.target, 'block target')
|
|
638
|
+
const requestedBlocks = normalizeRequestedBlocks(rawNames, { target })
|
|
492
639
|
const rootBlocks = []
|
|
493
640
|
|
|
494
|
-
for (const
|
|
495
|
-
const meta = await getMeta(name)
|
|
641
|
+
for (const requestedBlock of requestedBlocks) {
|
|
642
|
+
const meta = await getMeta(requestedBlock.name)
|
|
496
643
|
|
|
497
644
|
rootBlocks.push({
|
|
498
|
-
name,
|
|
499
|
-
variant:
|
|
645
|
+
name: requestedBlock.name,
|
|
646
|
+
variant: requestedBlock.variant,
|
|
647
|
+
target: requestedBlock.target,
|
|
648
|
+
config: { ...requestedBlock.config },
|
|
500
649
|
})
|
|
650
|
+
|
|
651
|
+
if (requestedBlock.variant && !meta.install?.variants?.[requestedBlock.variant]) {
|
|
652
|
+
throw new Error(`Variant "${requestedBlock.variant}" is not available for block "${requestedBlock.name}"`)
|
|
653
|
+
}
|
|
501
654
|
}
|
|
502
655
|
|
|
503
656
|
const plan = await resolveInstallPlan(rootBlocks)
|
|
@@ -505,13 +658,13 @@ export function createBlocksService({
|
|
|
505
658
|
await installPlan(plan)
|
|
506
659
|
|
|
507
660
|
const { configPath, config, blocks } = loadProjectConfig(projectRoot)
|
|
508
|
-
const nextBlocks =
|
|
509
|
-
...blocks.filter(block => !
|
|
661
|
+
const nextBlocks = dedupeBlocks([
|
|
662
|
+
...blocks.filter(block => !requestedBlocks.some(requestedBlock => requestedBlock.name === block.name)),
|
|
510
663
|
...rootBlocks,
|
|
511
664
|
])
|
|
512
665
|
|
|
513
666
|
saveProjectConfig(configPath, config, nextBlocks)
|
|
514
|
-
logger.info(`added ${rootBlocks.map(
|
|
667
|
+
logger.info(`added ${rootBlocks.map(formatConfiguredBlock).join(', ')}`)
|
|
515
668
|
|
|
516
669
|
return {
|
|
517
670
|
blocks: rootBlocks,
|
|
@@ -542,8 +695,26 @@ export function createBlocksService({
|
|
|
542
695
|
const beforePlan = await resolveInstallPlan(blocks)
|
|
543
696
|
const remainingBlocks = blocks.filter(block => !names.includes(block.name))
|
|
544
697
|
const afterPlan = remainingBlocks.length > 0 ? await resolveInstallPlan(remainingBlocks) : []
|
|
545
|
-
const
|
|
546
|
-
const removedItems = beforePlan.
|
|
698
|
+
const afterByKey = new Map(afterPlan.map(item => [toBlockKey(item), item]))
|
|
699
|
+
const removedItems = beforePlan.flatMap((item) => {
|
|
700
|
+
const nextItem = afterByKey.get(toBlockKey(item))
|
|
701
|
+
|
|
702
|
+
if (!nextItem) {
|
|
703
|
+
return [item]
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const afterFileKeys = new Set(nextItem.selectedFiles.map(toManagedFileKey))
|
|
707
|
+
const removedFiles = item.selectedFiles.filter(rule => !afterFileKeys.has(toManagedFileKey(rule)))
|
|
708
|
+
|
|
709
|
+
if (removedFiles.length === 0) {
|
|
710
|
+
return []
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return [{
|
|
714
|
+
...item,
|
|
715
|
+
selectedFiles: removedFiles,
|
|
716
|
+
}]
|
|
717
|
+
})
|
|
547
718
|
|
|
548
719
|
await removePlanItems(removedItems)
|
|
549
720
|
saveProjectConfig(configPath, config, remainingBlocks)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<section class="x-article-grid-horizontal grid grid-cols-container gap-y-12 md:gap-y-16 py-20 md:py-24 lg:py-28">
|
|
2
|
-
<div class="flex items-end gap-x-10 gap-y-6 w-full justify-between max-md:flex-col">
|
|
3
|
-
<div class="flex flex-col w-full lg:flex-1/2 items-start max-w-161">
|
|
4
|
-
<div class="x-badge mb-1 accent-main-secondary muted">
|
|
5
|
-
Section title
|
|
6
|
-
</div>
|
|
7
|
-
<h2 class="x-heading lg:lg mb-6">
|
|
8
|
-
Attention-grabbing medium length section headline
|
|
9
|
-
</h2>
|
|
10
|
-
<div class="x-text w-full">
|
|
11
|
-
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
12
|
-
Suspendisse varius enim in eros elementum tristique.
|
|
13
|
-
Duis cursus, mi quis viverra ornare, eros dolor interdum nulla.
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
<a href="#" class="x-button lg max-md:mr-auto">
|
|
17
|
-
View all
|
|
18
|
-
<svg class="me-auto size-5" aria-hidden="true">
|
|
19
|
-
<use href="#heroicons-mini/arrow-right"/>
|
|
20
|
-
</svg>
|
|
21
|
-
</a>
|
|
22
|
-
</div>
|
|
23
|
-
<div class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
24
|
-
{foreach range(1,$control->itemsCount ?? 6) as $i}
|
|
25
|
-
{include './ArticleItem.latte', itemVariant => $control->itemVariant, showAuthor => $control->showAuthor, lastHidden => true, mutedBadge => $control->itemVariant !== 'filled'}
|
|
26
|
-
{/foreach}
|
|
27
|
-
</div>
|
|
28
|
-
</section>
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
{default $lastHidden = false}
|
|
2
|
-
{default $itemVariant = 'basic'}
|
|
3
|
-
{default $mutedBadge = false}
|
|
4
|
-
{default $showAuthor = false}
|
|
5
|
-
|
|
6
|
-
<a
|
|
7
|
-
n:class="'x-article-item group flex flex-col', $lastHidden ? 'max-md:last:hidden lg:last:hidden'"
|
|
8
|
-
href="/article/detail.html"
|
|
9
|
-
>
|
|
10
|
-
<picture
|
|
11
|
-
n:class="
|
|
12
|
-
'x-image w-full before:skeleton aspect-310/207 md:aspect-310/207 lg:aspect-418/279 overflow-hidden',
|
|
13
|
-
$itemVariant === 'basic' ? 'rounded-2xl',
|
|
14
|
-
$itemVariant === 'outlined' ? 'rounded-t-2xl border-x border-t border-body-secondary',
|
|
15
|
-
$itemVariant === 'filled' ? 'rounded-t-2xl',
|
|
16
|
-
"
|
|
17
|
-
>
|
|
18
|
-
<source srcset="{=placeholder(345,431)}" media="(width < {$media->sm})">
|
|
19
|
-
<img
|
|
20
|
-
class="size-full object-cover group-hocus:scale-105 transition-transform"
|
|
21
|
-
src="{=placeholder(532,655)}"
|
|
22
|
-
alt=" " width="532" height="655" loading="lazy"
|
|
23
|
-
>
|
|
24
|
-
</picture>
|
|
25
|
-
<div
|
|
26
|
-
n:class="
|
|
27
|
-
'flex flex-col items-start pt-6 grow',
|
|
28
|
-
$itemVariant === 'outlined' ? 'rounded-b-2xl border-x border-b border-body-secondary px-6 pb-4',
|
|
29
|
-
$itemVariant === 'filled' ? 'bg-primary/5 px-6 pb-4 rounded-b-2xl',
|
|
30
|
-
"
|
|
31
|
-
>
|
|
32
|
-
<div class="flex gap-2 flex-wrap mb-3">
|
|
33
|
-
<div n:class="'x-badge sm', $mutedBadge ? 'muted' : ''">Article category</div>
|
|
34
|
-
<div class="x-badge sm muted bg-transparent" n:if="!$showAuthor">5 min read</div>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="x-heading xs mb-4 tracking-[-0.5px]">
|
|
37
|
-
Article title goes here
|
|
38
|
-
</div>
|
|
39
|
-
<div class="x-text text-main-secondary line-clamp-3 mb-4">
|
|
40
|
-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.
|
|
41
|
-
</div>
|
|
42
|
-
<div class="grid grid-cols-[auto_1fr] mb-4 gap-x-4" n:if="$showAuthor">
|
|
43
|
-
<picture class="x-image before:skeleton size-12 rounded-full aspect-square shrink-0 row-span-2 overflow-hidden">
|
|
44
|
-
<img class="object-cover" src="{=placeholder(48,48)}" alt=" " width="48" height="48" loading="lazy">
|
|
45
|
-
</picture>
|
|
46
|
-
<span class="x-text sm font-medium self-end">Author Name</span>
|
|
47
|
-
<div class="flex gap-4">
|
|
48
|
-
<span class="x-text xs text-main-tertiary font-medium">04.12.2025</span>
|
|
49
|
-
<span class="x-text xs text-main-tertiary font-medium">5 min read</span>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
<span class="x-button ghosted pl-1 mt-auto">
|
|
53
|
-
Read more
|
|
54
|
-
<svg class="me-auto size-5 group-hocus:translate-x-1 transition-transform" aria-hidden="true">
|
|
55
|
-
<use href="#heroicons-mini/arrow-right"/>
|
|
56
|
-
</svg>
|
|
57
|
-
</span>
|
|
58
|
-
</div>
|
|
59
|
-
</a>
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
<header class="x-header-nav-right h-(--x-header-height) grid grid-cols-container bg-body-primary sticky top-0 z-40">
|
|
2
|
-
<div class="flex items-center gap-1.5 md:gap-3">
|
|
3
|
-
<a href="#" class="shrink-0 max-lg:mr-auto lg:mr-7">
|
|
4
|
-
<img class="w-25 h-8" src="{placeholder(100,32)}" loading="lazy" alt="Logo">
|
|
5
|
-
</a>
|
|
6
|
-
<nav class="flex items-center flex-wrap justify-start gap-x-1 max-lg:hidden mr-auto">
|
|
7
|
-
<div class="x-popover trigger-hover group" n:foreach="range(1,4) as $item">
|
|
8
|
-
<a href="#" class="x-text x-link sm font-medium py-1 px-3 flex items-center gap-1.5">
|
|
9
|
-
Menu item {$iterator->counter}
|
|
10
|
-
<svg class="size-4 transition group-hocus:-scale-y-100" n:if="$iterator->last">
|
|
11
|
-
<use href="#heroicons-solid/chevron-down" />
|
|
12
|
-
</svg>
|
|
13
|
-
</a>
|
|
14
|
-
<div
|
|
15
|
-
n:class="
|
|
16
|
-
'x-nav-popover x-popover-content bottom-end border border-body-secondary shadow-lg min-w-55',
|
|
17
|
-
'm-1.5 p-4 flex flex-col gap-1 before:-top-2.5 before:h-2.5'
|
|
18
|
-
"
|
|
19
|
-
n:if="$iterator->last"
|
|
20
|
-
>
|
|
21
|
-
<a href="#" class="x-text x-link sm font-medium py-1 flex items-center gap-1.5" n:foreach="range(1,4) as $item">
|
|
22
|
-
Menu item
|
|
23
|
-
</a>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
</nav>
|
|
27
|
-
<div class="x-popover trigger-focus group">
|
|
28
|
-
<button class="x-text x-link sm font-medium py-1 px-3 flex items-center gap-1.5" type="button" tabindex="0">
|
|
29
|
-
<img
|
|
30
|
-
class="aspect-4/3 size-4"
|
|
31
|
-
src="https://cdn.jsdelivr.net/npm/flag-icons@7.5.0/flags/1x1/gb.svg"
|
|
32
|
-
alt="at"
|
|
33
|
-
loading="lazy"
|
|
34
|
-
>
|
|
35
|
-
EN
|
|
36
|
-
<svg class="size-4 transition group-focus-within:-scale-y-100">
|
|
37
|
-
<use href="#heroicons-solid/chevron-down" />
|
|
38
|
-
</svg>
|
|
39
|
-
</button>
|
|
40
|
-
<div class="x-popover-content bottom-end border border-body-secondary shadow-lg m-1.5 p-4 flex flex-col gap-1">
|
|
41
|
-
<a href="#" class="x-text x-link sm font-medium py-1 flex items-center gap-1.5" n:foreach="range(1,4) as $item">
|
|
42
|
-
<img
|
|
43
|
-
class="aspect-4/3 size-4"
|
|
44
|
-
src="https://cdn.jsdelivr.net/npm/flag-icons@7.5.0/flags/1x1/gb.svg"
|
|
45
|
-
alt="at"
|
|
46
|
-
loading="lazy"
|
|
47
|
-
>
|
|
48
|
-
English
|
|
49
|
-
</a>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
<a href="#" class="x-button sm max-md:hidden">
|
|
53
|
-
Call to action
|
|
54
|
-
</a>
|
|
55
|
-
<button
|
|
56
|
-
class="x-button square sm lg:hidden group swap"
|
|
57
|
-
type="button"
|
|
58
|
-
data-invoke-action="x-drawer#toggle"
|
|
59
|
-
data-invoke-target="#navDrawer"
|
|
60
|
-
aria-controls="navDrawer"
|
|
61
|
-
aria-expanded="false"
|
|
62
|
-
>
|
|
63
|
-
<svg class="size-5 shrink-0 not-group-aria-expanded:opacity-100 group-aria-expanded:rotate-45" aria-hidden="true">
|
|
64
|
-
<use href="#heroicons-solid/bars-3"></use>
|
|
65
|
-
</svg>
|
|
66
|
-
<svg class="size-5 shrink-0 not-group-aria-expanded:-rotate-45 group-aria-expanded:opacity-100" aria-hidden="true">
|
|
67
|
-
<use href="#heroicons-solid/x-mark"></use>
|
|
68
|
-
</svg>
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
</header>
|