windowpp 0.1.1 → 0.1.3

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/lib/create.js CHANGED
@@ -1,283 +1,98 @@
1
- #!/usr/bin/env node
2
- // cli/lib/create.js — WindowPP app scaffolding
3
- //
4
- // Copies a template from cli/templates/<template>/ to <outDir>/<name>/,
5
- // substituting {{APP_NAME}}, {{APP_TITLE}}, and {{CMAKE_TARGET}} in all
6
- // text files. Each created app is a self-contained project; it does NOT
7
- // need to live inside the framework repo.
8
- //
9
- // Usage (via CLI):
10
- // windowpp create my-app [--template solid] [--out-dir .]
11
-
12
- 'use strict';
13
-
14
- const path = require('path');
15
- const fs = require('fs');
16
- const { execSync } = require('child_process');
17
-
18
- function create(name, options = {}) {
19
- const {
20
- template = 'solid',
21
- outDir = process.cwd(),
22
- installDeps = true,
23
- } = options;
24
-
25
- if (!name || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
26
- console.error('Error: app name must start with a letter and contain only letters, digits, hyphens or underscores.');
27
- process.exit(1);
28
- }
29
-
30
- const appDir = path.join(outDir, name);
31
- if (fs.existsSync(appDir)) {
32
- console.error(`Error: directory "${appDir}" already exists.`);
33
- process.exit(1);
34
- }
35
-
36
- const cliDir = path.resolve(__dirname, '..');
37
- const templateDir = path.join(cliDir, 'templates', template);
38
- if (!fs.existsSync(templateDir)) {
39
- console.error(`Error: template "${template}" not found in ${path.join(cliDir, 'templates')}`);
40
- process.exit(1);
41
- }
42
-
43
- const cmakeTarget = name.replace(/-/g, '_');
44
- const appTitle = name
45
- .split(/[-_]/)
46
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
47
- .join(' ');
48
-
49
- const tokens = {
50
- '{{APP_NAME}}': name,
51
- '{{APP_TITLE}}': appTitle,
52
- '{{CMAKE_TARGET}}': cmakeTarget,
53
- };
54
-
55
- console.log(`\n=== WindowPP Create — ${name} (template: ${template}) ===\n`);
56
- console.log(`Scaffolding into: ${appDir}\n`);
57
-
58
- // ── Copy template ─────────────────────────────────────────────────────────
59
- copyDir(templateDir, appDir, tokens);
60
-
61
- // ── Install frontend deps ─────────────────────────────────────────────────
62
- const frontendDir = path.join(appDir, 'frontend');
63
- if (installDeps && fs.existsSync(frontendDir)) {
64
- console.log('Installing frontend dependencies...');
65
- execSync('npm install', { cwd: frontendDir, stdio: 'inherit' });
66
- }
67
-
68
- console.log(`\n✓ Created "${name}"\n`);
69
- console.log('Next steps:');
70
- console.log(` cd ${name}`);
71
- console.log(' windowpp dev\n');
72
- }
73
-
74
- // ── Helpers ───────────────────────────────────────────────────────────────────
75
-
76
- function copyDir(src, dest, tokens) {
77
- fs.mkdirSync(dest, { recursive: true });
78
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
79
- const srcPath = path.join(src, entry.name);
80
- const destPath = path.join(dest, entry.name);
81
- if (entry.isDirectory()) {
82
- copyDir(srcPath, destPath, tokens);
83
- } else {
84
- copyFile(srcPath, destPath, tokens);
85
- }
86
- }
87
- }
88
-
89
- const TEXT_EXTENSIONS = new Set([
90
- '.cpp', '.h', '.ts', '.tsx', '.js', '.json', '.html',
91
- '.css', '.md', '.txt', '.cmake', '.sh', '.env',
92
- ]);
93
-
94
- function copyFile(src, dest, tokens) {
95
- const ext = path.extname(src).toLowerCase();
96
- if (TEXT_EXTENSIONS.has(ext)) {
97
- let content = fs.readFileSync(src, 'utf8');
98
- for (const [token, value] of Object.entries(tokens)) {
99
- content = content.split(token).join(value);
100
- }
101
- fs.writeFileSync(dest, content, 'utf8');
102
- } else {
103
- fs.copyFileSync(src, dest);
104
- }
105
- }
106
-
107
- module.exports = { create };
108
-
109
-
110
- function create(name, options = {}) {
111
- const {
112
- template = 'solid',
113
- outDir = process.cwd(),
114
- installDeps = true,
115
- } = options;
116
-
117
- if (!name || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
118
- console.error('Error: app name must start with a letter and contain only letters, digits, hyphens or underscores.');
119
- process.exit(1);
120
- }
121
-
122
- const appDir = path.join(outDir, name);
123
- if (fs.existsSync(appDir)) {
124
- console.error(`Error: directory "${appDir}" already exists.`);
125
- process.exit(1);
126
- }
127
-
128
- const cliDir = path.resolve(__dirname, '..');
129
- const templateDir = path.join(cliDir, 'templates', template);
130
- if (!fs.existsSync(templateDir)) {
131
- console.error(`Error: template "${template}" not found in ${path.join(cliDir, 'templates')}`);
132
- process.exit(1);
133
- }
134
-
135
- const rootDir = findRootDir(outDir);
136
-
137
- // cmake target name: replace hyphens with underscores
138
- const cmakeTarget = name.replace(/-/g, '_');
139
- const appTitle = name
140
- .split(/[-_]/)
141
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
142
- .join(' ');
143
-
144
- const tokens = {
145
- '{{APP_NAME}}': name,
146
- '{{APP_TITLE}}': appTitle,
147
- '{{CMAKE_TARGET}}': cmakeTarget,
148
- '{{REPO_ROOT}}': rootDir.replace(/\\/g, '/'),
149
- };
150
-
151
- console.log(`\n=== WindowPP Create — ${name} (template: ${template}) ===\n`);
152
- console.log(`Scaffolding into: ${appDir}\n`);
153
-
154
- // ── Copy template ─────────────────────────────────────────────────────────
155
- copyDir(templateDir, appDir, tokens);
156
-
157
- // ── Register in CMakeLists.txt ────────────────────────────────────────────
158
- const appDirRelative = path.relative(rootDir, appDir).replace(/\\/g, '/');
159
- addToCMake(rootDir, name, cmakeTarget, appDirRelative);
160
-
161
- // ── Install frontend deps ─────────────────────────────────────────────────
162
- const frontendDir = path.join(appDir, 'frontend');
163
- if (installDeps && fs.existsSync(frontendDir)) {
164
- console.log('Installing frontend dependencies...');
165
- execSync('npm install', { cwd: frontendDir, stdio: 'inherit' });
166
- }
167
-
168
- console.log(`\n✓ Created "${name}"\n`);
169
- console.log('Next steps:');
170
- console.log(` cd ${name}`);
171
- console.log(' windowpp dev\n');
172
- }
173
-
174
- // ── Helpers ───────────────────────────────────────────────────────────────────
175
-
176
- function copyDir(src, dest, tokens) {
177
- fs.mkdirSync(dest, { recursive: true });
178
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
179
- const srcPath = path.join(src, entry.name);
180
- const destPath = path.join(dest, entry.name);
181
- if (entry.isDirectory()) {
182
- copyDir(srcPath, destPath, tokens);
183
- } else {
184
- copyFile(srcPath, destPath, tokens);
185
- }
186
- }
187
- }
188
-
189
- const TEXT_EXTENSIONS = new Set([
190
- '.cpp', '.h', '.ts', '.tsx', '.js', '.json', '.html',
191
- '.css', '.md', '.txt', '.cmake', '.sh', '.env',
192
- ]);
193
-
194
- function copyFile(src, dest, tokens) {
195
- const ext = path.extname(src).toLowerCase();
196
- if (TEXT_EXTENSIONS.has(ext)) {
197
- let content = fs.readFileSync(src, 'utf8');
198
- for (const [token, value] of Object.entries(tokens)) {
199
- content = content.split(token).join(value);
200
- }
201
- fs.writeFileSync(dest, content, 'utf8');
202
- } else {
203
- fs.copyFileSync(src, dest);
204
- }
205
- }
206
-
207
- function addToCMake(rootDir, appName, cmakeTarget, appDirRelative) {
208
- const cmakePath = path.join(rootDir, 'CMakeLists.txt');
209
- let cmake = fs.readFileSync(cmakePath, 'utf8');
210
-
211
- // Insert before the # ─── Install section
212
- const installMarker = '# ─── Install ──';
213
- if (!cmake.includes(installMarker)) {
214
- console.warn('Warning: could not locate Install section in CMakeLists.txt — skipping auto-registration.');
215
- return;
216
- }
217
-
218
- const appBlock = generateCMakeBlock(appName, cmakeTarget, appDirRelative);
219
- cmake = cmake.replace(installMarker, appBlock + '\n' + installMarker);
220
- fs.writeFileSync(cmakePath, cmake, 'utf8');
221
- console.log(`Registered ${cmakeTarget} in CMakeLists.txt`);
222
- }
223
-
224
- function generateCMakeBlock(appName, cmakeTarget, appDirRelative) {
225
- const generatedVar = cmakeTarget.toUpperCase() + '_GENERATED_DIR';
226
- const embeddedCppVar = cmakeTarget.toUpperCase() + '_EMBEDDED_ASSETS_CPP';
227
- const embeddedHVar = cmakeTarget.toUpperCase() + '_EMBEDDED_ASSETS_H';
228
- const assetsTarget = cmakeTarget + '_frontend_assets';
229
-
230
- return `# ─── ${appName} ──────────────────────────────────────────────────────────────────
231
-
232
- if(WPP_BUILD_EXAMPLES)
233
- add_executable(${cmakeTarget} ${appDirRelative}/main.cpp)
234
- target_link_libraries(${cmakeTarget} PRIVATE windowpp nlohmann_json::nlohmann_json)
235
- if(WIN32 AND WEBVIEW2_INCLUDE_DIR)
236
- target_include_directories(${cmakeTarget} PRIVATE \${WEBVIEW2_INCLUDE_DIR})
237
- if(WEBVIEW2_STATIC)
238
- target_link_libraries(${cmakeTarget} PRIVATE
239
- \${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/webview/x64/WebView2LoaderStatic.lib
240
- version)
241
- else()
242
- target_link_libraries(${cmakeTarget} PRIVATE
243
- \${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/webview/x64/WebView2Loader.lib)
244
- endif()
245
- endif()
246
-
247
- set(${generatedVar} \${CMAKE_BINARY_DIR}/generated_${cmakeTarget})
248
- set(${embeddedCppVar} \${${generatedVar}}/embedded_assets.cpp)
249
- set(${embeddedHVar} \${${generatedVar}}/embedded_assets.h)
250
-
251
- add_custom_command(
252
- OUTPUT \${${embeddedCppVar}} \${${embeddedHVar}}
253
- COMMAND \${CMAKE_COMMAND} -E make_directory \${${generatedVar}}
254
- COMMAND \${Python3_EXECUTABLE}
255
- \${WPP_EMBED_SCRIPT}
256
- \${CMAKE_CURRENT_SOURCE_DIR}/${appDirRelative}/frontend/dist
257
- \${${embeddedCppVar}}
258
- \${${embeddedHVar}}
259
- DEPENDS \${CMAKE_CURRENT_SOURCE_DIR}/${appDirRelative}/frontend/dist/index.html
260
- COMMENT "Embedding ${appName} frontend assets into C++ byte arrays..."
261
- VERBATIM
262
- )
263
-
264
- set_source_files_properties(
265
- \${${embeddedCppVar}}
266
- \${${embeddedHVar}}
267
- PROPERTIES GENERATED TRUE
268
- )
269
-
270
- add_custom_target(${assetsTarget} ALL
271
- DEPENDS \${${embeddedCppVar}} \${${embeddedHVar}}
272
- )
273
-
274
- target_sources(${cmakeTarget} PRIVATE \${${embeddedCppVar}})
275
- target_include_directories(${cmakeTarget} PRIVATE \${${generatedVar}})
276
- target_compile_definitions(${cmakeTarget} PRIVATE WPP_EMBEDDED_ASSETS)
277
- add_dependencies(${cmakeTarget} ${assetsTarget})
278
- endif()
279
-
280
- `;
281
- }
282
-
283
- module.exports = { create };
1
+ #!/usr/bin/env node
2
+ // cli/lib/create.js — WindowPP app scaffolding
3
+
4
+ 'use strict';
5
+
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const { execSync } = require('child_process');
9
+
10
+ const TEXT_EXTENSIONS = new Set([
11
+ '.cpp', '.h', '.ts', '.tsx', '.js', '.json', '.html',
12
+ '.css', '.md', '.txt', '.cmake', '.sh', '.env',
13
+ ]);
14
+
15
+ function copyFile(src, dest, tokens) {
16
+ const ext = path.extname(src).toLowerCase();
17
+ if (TEXT_EXTENSIONS.has(ext)) {
18
+ let content = fs.readFileSync(src, 'utf8');
19
+ for (const [token, value] of Object.entries(tokens)) {
20
+ content = content.split(token).join(value);
21
+ }
22
+ fs.writeFileSync(dest, content, 'utf8');
23
+ } else {
24
+ fs.copyFileSync(src, dest);
25
+ }
26
+ }
27
+
28
+ function copyDir(src, dest, tokens) {
29
+ fs.mkdirSync(dest, { recursive: true });
30
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
31
+ const srcPath = path.join(src, entry.name);
32
+ const destPath = path.join(dest, entry.name);
33
+ if (entry.isDirectory()) {
34
+ copyDir(srcPath, destPath, tokens);
35
+ } else {
36
+ copyFile(srcPath, destPath, tokens);
37
+ }
38
+ }
39
+ }
40
+
41
+ function create(name, options = {}) {
42
+ const {
43
+ template = 'solid',
44
+ outDir = process.cwd(),
45
+ installDeps = true,
46
+ } = options;
47
+
48
+ if (!name || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
49
+ console.error('Error: app name must start with a letter and contain only letters, digits, hyphens or underscores.');
50
+ process.exit(1);
51
+ }
52
+
53
+ const appDir = path.join(outDir, name);
54
+ if (fs.existsSync(appDir)) {
55
+ console.error(`Error: directory "${appDir}" already exists.`);
56
+ process.exit(1);
57
+ }
58
+
59
+ const cliDir = path.resolve(__dirname, '..');
60
+ const templateDir = path.join(cliDir, 'templates', template);
61
+ if (!fs.existsSync(templateDir)) {
62
+ console.error(`Error: template "${template}" not found in ${path.join(cliDir, 'templates')}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ const cmakeTarget = name.replace(/-/g, '_');
67
+ const appTitle = name
68
+ .split(/[-_]/)
69
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
70
+ .join(' ');
71
+
72
+ const tokens = {
73
+ '{{APP_NAME}}': name,
74
+ '{{APP_TITLE}}': appTitle,
75
+ '{{CMAKE_TARGET}}': cmakeTarget,
76
+ // Resolved at create-time so vite.config / API.ts can import from the
77
+ // installed framework's src/ tree via the @wpp alias.
78
+ '{{REPO_ROOT}}': path.join(cliDir, 'framework').replace(/\\/g, '/'),
79
+ };
80
+
81
+ console.log(`\n=== WindowPP Create --- ${name} (template: ${template}) ===\n`);
82
+ console.log(`Scaffolding into: ${appDir}\n`);
83
+
84
+ copyDir(templateDir, appDir, tokens);
85
+
86
+ const frontendDir = path.join(appDir, 'frontend');
87
+ if (installDeps && fs.existsSync(frontendDir)) {
88
+ console.log('Installing frontend dependencies...');
89
+ execSync('npm install', { cwd: frontendDir, stdio: 'inherit' });
90
+ }
91
+
92
+ console.log(`\n-- Created "${name}"\n`);
93
+ console.log('Next steps:');
94
+ console.log(` cd ${name}`);
95
+ console.log(' windowpp dev\n');
96
+ }
97
+
98
+ module.exports = { create };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windowpp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "WindowPP CLI — build, dev, and scaffold for WindowPP apps",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -16,7 +16,9 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "sync": "node scripts/sync-framework.js",
19
- "prepublishOnly": "node scripts/sync-framework.js"
19
+ "sync:templates": "node scripts/sync-templates.js",
20
+ "sync:all": "node scripts/sync-framework.js && node scripts/sync-templates.js",
21
+ "prepublishOnly": "node scripts/sync-framework.js && node scripts/sync-templates.js"
20
22
  },
21
23
  "engines": {
22
24
  "node": ">=18"
@@ -47,6 +47,10 @@ console.log(`\n=== windowpp publish: ${pkg.version} → ${nextVersion} (${bumpTy
47
47
  console.log('Syncing framework source...');
48
48
  execSync('node scripts/sync-framework.js', { cwd: CLI_DIR, stdio: 'inherit' });
49
49
 
50
+ // ── Sync templates from monorepo apps ────────────────────────────────────────
51
+ console.log('\nSyncing templates...');
52
+ execSync('node scripts/sync-templates.js', { cwd: CLI_DIR, stdio: 'inherit' });
53
+
50
54
  // ── Publish with the new version (without writing it yet) ─────────────────────
51
55
  // Temporarily write the new version so npm publish picks it up, then revert on failure.
52
56
  pkg.version = nextVersion;
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ // cli/scripts/sync-templates.js
3
+ //
4
+ // Syncs app directories from the monorepo into cli/templates/ as reusable
5
+ // project templates. Called manually or as part of the publish flow.
6
+ //
7
+ // Usage:
8
+ // node cli/scripts/sync-templates.js — sync all templates
9
+ // node cli/scripts/sync-templates.js example — sync one by name
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+
16
+ const SCRIPT_DIR = __dirname;
17
+ const CLI_DIR = path.join(SCRIPT_DIR, '..');
18
+ const REPO_ROOT = path.join(CLI_DIR, '..');
19
+ const TEMPLATES_DIR = path.join(CLI_DIR, 'templates');
20
+ const BASE_TEMPLATE = path.join(TEMPLATES_DIR, 'solid');
21
+
22
+ // ─── Template source configuration ───────────────────────────────────────────
23
+ //
24
+ // Each entry describes one template that can be used with `windowpp create`.
25
+ // To register a new app as a template, add an entry here and run this script.
26
+ //
27
+ // Fields:
28
+ // name — template dir name → cli/templates/<name>/
29
+ // source — source dir relative to repo root
30
+ // appName — concrete npm package name in source (replaced → {{APP_NAME}})
31
+ // cmakeTarget — concrete cmake target in source (replaced → {{CMAKE_TARGET}})
32
+ // appTitle — human-readable title in source (replaced → {{APP_TITLE}})
33
+ // useBase — paths taken from the solid base template instead of source
34
+ // (these need standalone-specific content not in the monorepo app)
35
+ // skip — relative paths / bare file names to skip when copying from source
36
+
37
+ const TEMPLATE_SOURCES = [
38
+ {
39
+ name: 'example',
40
+ source: 'example-app',
41
+ appName: 'windowpp-example',
42
+ cmakeTarget: 'wpp_example',
43
+ appTitle: 'WindowPP Example',
44
+ // Use the standalone-compatible versions from the solid base template
45
+ useBase: [
46
+ 'CMakeLists.txt',
47
+ 'package.json',
48
+ 'frontend/vite.config.ts',
49
+ ],
50
+ // Skip monorepo-specific / generated / lock files
51
+ skip: [
52
+ 'build.js',
53
+ 'dev.js',
54
+ 'frontend/node_modules',
55
+ 'frontend/dist',
56
+ 'frontend/pnpm-lock.yaml',
57
+ 'frontend/package-lock.json',
58
+ 'frontend/.gitignore',
59
+ 'frontend/README.md',
60
+ ],
61
+ },
62
+ // ── To add more templates, copy an entry like this: ───────────────────────
63
+ // {
64
+ // name: 'icup',
65
+ // source: 'ICUP',
66
+ // appName: 'wpp-icup',
67
+ // cmakeTarget: 'wpp_icup',
68
+ // appTitle: 'WindowPP ICUP',
69
+ // useBase: ['CMakeLists.txt', 'package.json', 'frontend/vite.config.ts'],
70
+ // skip: ['build.js', 'dev.js', 'frontend/node_modules', 'frontend/dist'],
71
+ // },
72
+ ];
73
+
74
+ // ─── File extensions that get text/token substitution ────────────────────────
75
+ const TEXT_EXTENSIONS = new Set([
76
+ '.cpp', '.h', '.ts', '.tsx', '.js', '.json', '.html',
77
+ '.css', '.md', '.txt', '.cmake', '.sh', '.env',
78
+ ]);
79
+
80
+ // ─── Build the list of [from, to] string substitution pairs for an entry ─────
81
+ //
82
+ // Transforms applied to all text files:
83
+ //
84
+ // 1. Concrete app name / cmake target / title → template tokens
85
+ //
86
+ // 2. TypeScript relative imports from frontend/src/ depth (3 levels up to src/):
87
+ // '../../../src/Foo' → '@wpp/Foo'
88
+ // Covered by the vite alias: '@wpp' → resolve(REPO_ROOT, 'src')
89
+ //
90
+ // 3. TypeScript relative imports from frontend/ depth (2 levels up):
91
+ // '../../src/Foo' → '@wpp/Foo'
92
+ //
93
+ // 4. C++ #include relative back to src/:
94
+ // "#include "../src/Foo/bar.h"" → "#include "Foo/bar.h""
95
+ // Works because CMakeLists adds target_include_directories(...PRIVATE "${WPP_FRAMEWORK_DIR}/src")
96
+
97
+ function buildTokens(entry) {
98
+ return [
99
+ // ── Name tokens (longer strings first to avoid partial matches) ─────
100
+ [entry.appName, '{{APP_NAME}}'],
101
+ [entry.cmakeTarget, '{{CMAKE_TARGET}}'],
102
+ [entry.appTitle, '{{APP_TITLE}}'],
103
+
104
+ // ── TypeScript/TSX: monorepo-relative imports from frontend/src/ ────
105
+ ["'../../../src/", "'@wpp/"],
106
+ ['"../../../src/', '"@wpp/'],
107
+
108
+ // ── TypeScript/TSX: from frontend/ level ───────────────────────────
109
+ ["'../../src/", "'@wpp/"],
110
+ ['"../../src/', '"@wpp/'],
111
+
112
+ // ── C++ includes: "../src/SubDir/file.h" → "SubDir/file.h" ─────────
113
+ // (CMakeLists adds ${WPP_FRAMEWORK_DIR}/src as a private include dir)
114
+ ['"../src/', '"'],
115
+ ];
116
+ }
117
+
118
+ function applyTokens(content, tokens) {
119
+ for (const [from, to] of tokens) {
120
+ content = content.split(from).join(to);
121
+ }
122
+ return content;
123
+ }
124
+
125
+ // ─── File copy helpers ───────────────────────────────────────────────────────
126
+ function copyFileTransformed(src, dest, tokens) {
127
+ const ext = path.extname(src).toLowerCase();
128
+ if (TEXT_EXTENSIONS.has(ext)) {
129
+ let content = fs.readFileSync(src, 'utf8');
130
+ content = applyTokens(content, tokens);
131
+ fs.writeFileSync(dest, content, 'utf8');
132
+ } else {
133
+ fs.copyFileSync(src, dest);
134
+ }
135
+ }
136
+
137
+ // Copy a directory recursively, respecting a per-directory skip set.
138
+ // relPath tracks the path relative to the template root for skip matching.
139
+ function copySourceDir(src, dest, tokens, skipRelPaths, relPrefix) {
140
+ fs.mkdirSync(dest, { recursive: true });
141
+ for (const dirent of fs.readdirSync(src, { withFileTypes: true })) {
142
+ const relPath = relPrefix ? `${relPrefix}/${dirent.name}` : dirent.name;
143
+ // Skip if matched by bare name OR by relative path
144
+ if (skipRelPaths.has(dirent.name) || skipRelPaths.has(relPath)) continue;
145
+
146
+ const srcPath = path.join(src, dirent.name);
147
+ const destPath = path.join(dest, dirent.name);
148
+
149
+ if (dirent.isDirectory()) {
150
+ copySourceDir(srcPath, destPath, tokens, skipRelPaths, relPath);
151
+ } else {
152
+ copyFileTransformed(srcPath, destPath, tokens);
153
+ }
154
+ }
155
+ }
156
+
157
+ // ─── Sync a single template entry ────────────────────────────────────────────
158
+ function syncTemplate(entry) {
159
+ const srcDir = path.join(REPO_ROOT, entry.source);
160
+ const destDir = path.join(TEMPLATES_DIR, entry.name);
161
+
162
+ if (!fs.existsSync(srcDir)) {
163
+ console.warn(` ⚠ source not found: ${entry.source} — skipping`);
164
+ return 0;
165
+ }
166
+
167
+ // Clear and recreate the destination template directory
168
+ if (fs.existsSync(destDir)) {
169
+ fs.rmSync(destDir, { recursive: true, force: true });
170
+ }
171
+ fs.mkdirSync(destDir, { recursive: true });
172
+
173
+ const tokens = buildTokens(entry);
174
+ const useBaseSet = new Set(entry.useBase ?? []);
175
+ const skipRelPaths = new Set(entry.skip ?? []);
176
+
177
+ // Also skip the "useBase" paths when copying from source
178
+ for (const rel of useBaseSet) skipRelPaths.add(rel);
179
+
180
+ let count = 0;
181
+
182
+ // 1. Overlay "base" files from cli/templates/solid/ (with this entry's tokens)
183
+ for (const rel of useBaseSet) {
184
+ const baseSrc = path.join(BASE_TEMPLATE, rel);
185
+ const baseDest = path.join(destDir, rel);
186
+ if (!fs.existsSync(baseSrc)) {
187
+ console.warn(` ⚠ base file not found: ${rel}`);
188
+ continue;
189
+ }
190
+ fs.mkdirSync(path.dirname(baseDest), { recursive: true });
191
+ copyFileTransformed(baseSrc, baseDest, tokens);
192
+ count++;
193
+ }
194
+
195
+ // 2. Copy everything else from the source app
196
+ copySourceDir(srcDir, destDir, tokens, skipRelPaths, '');
197
+
198
+ // Count total destination files
199
+ count = countFiles(destDir);
200
+ return count;
201
+ }
202
+
203
+ function countFiles(dir) {
204
+ let n = 0;
205
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
206
+ if (dirent.isDirectory()) n += countFiles(path.join(dir, dirent.name));
207
+ else n++;
208
+ }
209
+ return n;
210
+ }
211
+
212
+ // ─── Entry point ─────────────────────────────────────────────────────────────
213
+ function run() {
214
+ const filter = process.argv[2]; // optional: name of one template to sync
215
+ const targets = filter
216
+ ? TEMPLATE_SOURCES.filter(e => e.name === filter)
217
+ : TEMPLATE_SOURCES;
218
+
219
+ if (filter && targets.length === 0) {
220
+ const names = TEMPLATE_SOURCES.map(e => e.name).join(', ');
221
+ console.error(`\nNo template named "${filter}". Available: ${names || '(none)'}\n`);
222
+ process.exit(1);
223
+ }
224
+
225
+ console.log('\n=== windowpp: syncing templates ===\n');
226
+
227
+ let total = 0;
228
+ for (const entry of targets) {
229
+ process.stdout.write(` ${entry.source} → templates/${entry.name}/ ...`);
230
+ const n = syncTemplate(entry);
231
+ process.stdout.write(` ${n} file(s)\n`);
232
+ total += n;
233
+ }
234
+
235
+ console.log(`\n✓ Synced ${targets.length} template(s) (${total} total files)\n`);
236
+ }
237
+
238
+ run();