pulse-js-framework 1.11.3 → 1.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/cli/analyze.js +21 -8
  2. package/cli/build.js +83 -56
  3. package/cli/dev.js +108 -94
  4. package/cli/docs-test.js +52 -33
  5. package/cli/index.js +81 -51
  6. package/cli/mobile.js +92 -40
  7. package/cli/release.js +64 -46
  8. package/cli/scaffold.js +14 -13
  9. package/compiler/lexer.js +55 -54
  10. package/compiler/parser/core.js +1 -0
  11. package/compiler/parser/state.js +6 -12
  12. package/compiler/parser/style.js +17 -20
  13. package/compiler/parser/view.js +1 -3
  14. package/compiler/preprocessor.js +124 -262
  15. package/compiler/sourcemap.js +10 -4
  16. package/compiler/transformer/expressions.js +122 -106
  17. package/compiler/transformer/index.js +2 -4
  18. package/compiler/transformer/style.js +74 -7
  19. package/compiler/transformer/view.js +86 -36
  20. package/loader/esbuild-plugin-server-components.js +209 -0
  21. package/loader/esbuild-plugin.js +41 -93
  22. package/loader/parcel-plugin.js +37 -97
  23. package/loader/rollup-plugin-server-components.js +30 -169
  24. package/loader/rollup-plugin.js +27 -78
  25. package/loader/shared.js +362 -0
  26. package/loader/swc-plugin.js +65 -82
  27. package/loader/vite-plugin-server-components.js +30 -171
  28. package/loader/vite-plugin.js +25 -10
  29. package/loader/webpack-loader-server-components.js +21 -134
  30. package/loader/webpack-loader.js +25 -80
  31. package/package.json +52 -12
  32. package/runtime/dom-selector.js +2 -1
  33. package/runtime/form.js +4 -3
  34. package/runtime/http.js +6 -1
  35. package/runtime/logger.js +44 -24
  36. package/runtime/router/utils.js +14 -7
  37. package/runtime/security.js +13 -1
  38. package/runtime/server-components/actions-server.js +23 -19
  39. package/runtime/server-components/error-sanitizer.js +18 -18
  40. package/runtime/server-components/security.js +41 -24
  41. package/runtime/ssr-preload.js +5 -3
  42. package/runtime/testing.js +759 -0
  43. package/runtime/utils.js +3 -2
  44. package/server/utils.js +15 -9
  45. package/sw/index.js +2 -0
  46. package/types/loaders.d.ts +1043 -0
  47. package/compiler/parser/_extract.js +0 -393
  48. package/loader/README.md +0 -509
package/cli/docs-test.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - .pulse file compilation check
9
9
  */
10
10
 
11
- import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
11
+ import { readFileSync, readdirSync, accessSync } from 'fs';
12
12
  import { join, dirname, relative } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { createServer } from 'http';
@@ -27,18 +27,23 @@ const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'c
27
27
  * Collect all JS files recursively
28
28
  */
29
29
  function collectJsFiles(dir, files = []) {
30
- if (!existsSync(dir)) return files;
30
+ let entries;
31
+ try {
32
+ entries = readdirSync(dir, { withFileTypes: true });
33
+ } catch (e) {
34
+ if (e.code === 'ENOENT') return files;
35
+ throw e;
36
+ }
31
37
 
32
- for (const entry of readdirSync(dir)) {
38
+ for (const entry of entries) {
33
39
  // Skip common non-source directories
34
- if (SKIP_DIRS.includes(entry)) continue;
40
+ if (SKIP_DIRS.includes(entry.name)) continue;
35
41
 
36
- const fullPath = join(dir, entry);
37
- const stat = statSync(fullPath);
42
+ const fullPath = join(dir, entry.name);
38
43
 
39
- if (stat.isDirectory()) {
44
+ if (entry.isDirectory()) {
40
45
  collectJsFiles(fullPath, files);
41
- } else if (entry.endsWith('.js')) {
46
+ } else if (entry.name.endsWith('.js')) {
42
47
  files.push(fullPath);
43
48
  }
44
49
  }
@@ -50,18 +55,23 @@ function collectJsFiles(dir, files = []) {
50
55
  * Collect all .pulse files recursively
51
56
  */
52
57
  function collectPulseFiles(dir, files = []) {
53
- if (!existsSync(dir)) return files;
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(dir, { withFileTypes: true });
61
+ } catch (e) {
62
+ if (e.code === 'ENOENT') return files;
63
+ throw e;
64
+ }
54
65
 
55
- for (const entry of readdirSync(dir)) {
66
+ for (const entry of entries) {
56
67
  // Skip common non-source directories
57
- if (SKIP_DIRS.includes(entry)) continue;
68
+ if (SKIP_DIRS.includes(entry.name)) continue;
58
69
 
59
- const fullPath = join(dir, entry);
60
- const stat = statSync(fullPath);
70
+ const fullPath = join(dir, entry.name);
61
71
 
62
- if (stat.isDirectory()) {
72
+ if (entry.isDirectory()) {
63
73
  collectPulseFiles(fullPath, files);
64
- } else if (entry.endsWith('.pulse')) {
74
+ } else if (entry.name.endsWith('.pulse')) {
65
75
  files.push(fullPath);
66
76
  }
67
77
  }
@@ -194,10 +204,16 @@ function resolveImport(importPath, fromFile) {
194
204
 
195
205
  // Add .js extension if missing
196
206
  if (!resolved.endsWith('.js') && !resolved.endsWith('.pulse')) {
197
- if (existsSync(resolved + '.js')) {
207
+ try {
208
+ accessSync(resolved + '.js');
198
209
  resolved += '.js';
199
- } else if (existsSync(resolved + '/index.js')) {
200
- resolved = join(resolved, 'index.js');
210
+ } catch (e) {
211
+ try {
212
+ accessSync(resolved + '/index.js');
213
+ resolved = join(resolved, 'index.js');
214
+ } catch (_e) {
215
+ // Neither exists, return as-is
216
+ }
201
217
  }
202
218
  }
203
219
 
@@ -224,7 +240,9 @@ function validateImports(filePath) {
224
240
  // Skip pulse-js-framework imports
225
241
  if (importPath.includes('pulse-js-framework')) continue;
226
242
 
227
- if (!existsSync(resolved)) {
243
+ try {
244
+ accessSync(resolved);
245
+ } catch (_e) {
228
246
  errors.push({
229
247
  file: filePath,
230
248
  importPath,
@@ -310,21 +328,22 @@ function startTestServer(port = 0) {
310
328
 
311
329
  const filePath = join(docsDir, pathname);
312
330
 
313
- if (existsSync(filePath) && statSync(filePath).isFile()) {
314
- const content = readFileSync(filePath);
315
- const ext = pathname.split('.').pop();
316
- const mimeTypes = {
317
- 'html': 'text/html',
318
- 'js': 'application/javascript',
319
- 'css': 'text/css',
320
- 'json': 'application/json'
321
- };
322
- res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
323
- res.end(content);
324
- } else {
325
- res.writeHead(404);
326
- res.end('Not Found');
331
+ let content;
332
+ try {
333
+ content = readFileSync(filePath);
334
+ } catch (err) {
335
+ if (err.code === 'ENOENT' || err.code === 'EISDIR') { res.writeHead(404); res.end('Not Found'); return; }
336
+ throw err;
327
337
  }
338
+ const ext = pathname.split('.').pop();
339
+ const mimeTypes = {
340
+ 'html': 'text/html',
341
+ 'js': 'application/javascript',
342
+ 'css': 'text/css',
343
+ 'json': 'application/json'
344
+ };
345
+ res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
346
+ res.end(content);
328
347
  });
329
348
 
330
349
  // Disable keep-alive timeout
package/cli/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname, join, resolve, relative } from 'path';
9
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch, cpSync, statSync } from 'fs';
9
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, watch, accessSync } from 'fs';
10
10
  import { log } from './logger.js';
11
11
  import { findPulseFiles, parseArgs } from './utils/file-utils.js';
12
12
  import { runHelp } from './help.js';
@@ -218,7 +218,9 @@ function copyExampleTemplate(templateName, projectPath, projectName) {
218
218
  const examplesDir = join(__dirname, '..', 'examples');
219
219
  const templateDir = join(examplesDir, templateName);
220
220
 
221
- if (!existsSync(templateDir)) {
221
+ try {
222
+ accessSync(templateDir);
223
+ } catch (e) {
222
224
  throw new Error(`Template "${templateName}" not found at ${templateDir}`);
223
225
  }
224
226
 
@@ -226,9 +228,7 @@ function copyExampleTemplate(templateName, projectPath, projectName) {
226
228
  * Recursively copy directory, transforming JS files
227
229
  */
228
230
  function copyDir(src, dest) {
229
- if (!existsSync(dest)) {
230
- mkdirSync(dest, { recursive: true });
231
- }
231
+ mkdirSync(dest, { recursive: true });
232
232
 
233
233
  const entries = readdirSync(src, { withFileTypes: true });
234
234
 
@@ -272,17 +272,24 @@ function copyExampleTemplate(templateName, projectPath, projectName) {
272
272
 
273
273
  // Copy src directory
274
274
  const srcDir = join(templateDir, 'src');
275
- if (existsSync(srcDir)) {
275
+ try {
276
+ readdirSync(srcDir);
276
277
  copyDir(srcDir, join(projectPath, 'src'));
278
+ } catch (e) {
279
+ if (e.code !== 'ENOENT') throw e;
280
+ // No src directory in template, skip
277
281
  }
278
282
 
279
283
  // Copy index.html if exists
280
284
  const indexHtml = join(templateDir, 'index.html');
281
- if (existsSync(indexHtml)) {
285
+ try {
282
286
  let content = readFileSync(indexHtml, 'utf-8');
283
287
  // Update title to project name
284
288
  content = content.replace(/<title>[^<]*<\/title>/, `<title>${projectName}</title>`);
285
289
  writeFileSync(join(projectPath, 'index.html'), content);
290
+ } catch (e) {
291
+ if (e.code !== 'ENOENT') throw e;
292
+ // No index.html in template, skip
286
293
  }
287
294
 
288
295
  return true;
@@ -312,9 +319,13 @@ async function createProject(args) {
312
319
 
313
320
  const projectPath = resolve(process.cwd(), projectName);
314
321
 
315
- if (existsSync(projectPath)) {
322
+ try {
323
+ accessSync(projectPath);
316
324
  log.error(`Directory "${projectName}" already exists.`);
317
325
  process.exit(1);
326
+ } catch (e) {
327
+ if (e.code !== 'ENOENT') throw e;
328
+ // Path doesn't exist, safe to create
318
329
  }
319
330
 
320
331
  const useTypescript = options.typescript || options.ts || false;
@@ -675,34 +686,30 @@ async function initProject(args) {
675
686
  log.info(`Initializing Pulse project in current directory${useTypescript ? ' (TypeScript)' : ''}...`);
676
687
 
677
688
  // Create src directory if it doesn't exist
678
- if (!existsSync(join(cwd, 'src'))) {
679
- mkdirSync(join(cwd, 'src'));
680
- }
689
+ mkdirSync(join(cwd, 'src'), { recursive: true });
681
690
 
682
691
  // Create public directory if it doesn't exist
683
- if (!existsSync(join(cwd, 'public'))) {
684
- mkdirSync(join(cwd, 'public'));
685
- }
692
+ mkdirSync(join(cwd, 'public'), { recursive: true });
686
693
 
687
694
  // Check for existing package.json
688
695
  const pkgPath = join(cwd, 'package.json');
689
696
  let pkg = {};
690
697
 
691
- if (existsSync(pkgPath)) {
692
- try {
693
- const pkgContent = readFileSync(pkgPath, 'utf-8');
694
- if (!pkgContent.trim()) {
695
- log.warn('Existing package.json is empty, creating new one.');
696
- } else {
697
- pkg = JSON.parse(pkgContent);
698
- log.info('Found existing package.json, merging...');
699
- }
700
- } catch (e) {
701
- if (e instanceof SyntaxError) {
702
- log.warn(`Invalid JSON in package.json: ${e.message}. Creating new one.`);
703
- } else {
704
- log.warn(`Could not read package.json: ${e.message}. Creating new one.`);
705
- }
698
+ try {
699
+ const pkgContent = readFileSync(pkgPath, 'utf-8');
700
+ if (!pkgContent.trim()) {
701
+ log.warn('Existing package.json is empty, creating new one.');
702
+ } else {
703
+ pkg = JSON.parse(pkgContent);
704
+ log.info('Found existing package.json, merging...');
705
+ }
706
+ } catch (e) {
707
+ if (e.code === 'ENOENT') {
708
+ // No existing package.json, will create new one
709
+ } else if (e instanceof SyntaxError) {
710
+ log.warn(`Invalid JSON in package.json: ${e.message}. Creating new one.`);
711
+ } else {
712
+ log.warn(`Could not read package.json: ${e.message}. Creating new one.`);
706
713
  }
707
714
  }
708
715
 
@@ -742,7 +749,7 @@ async function initProject(args) {
742
749
  const viteConfigExt = useTypescript ? 'ts' : 'js';
743
750
  const viteConfigPath = join(cwd, `vite.config.${viteConfigExt}`);
744
751
 
745
- if (!existsSync(viteConfigPath) && !existsSync(join(cwd, 'vite.config.js')) && !existsSync(join(cwd, 'vite.config.ts'))) {
752
+ {
746
753
  const viteConfig = `import { defineConfig } from 'vite';
747
754
  import pulse from 'pulse-js-framework/vite';
748
755
 
@@ -750,12 +757,17 @@ export default defineConfig({
750
757
  plugins: [pulse()]
751
758
  });
752
759
  `;
753
- writeFileSync(viteConfigPath, viteConfig);
754
- log.success(`Created vite.config.${viteConfigExt}`);
760
+ // Use atomic wx flag to avoid TOCTOU race (no existsSync check needed)
761
+ try {
762
+ writeFileSync(viteConfigPath, viteConfig, { flag: 'wx' });
763
+ log.success(`Created vite.config.${viteConfigExt}`);
764
+ } catch (e) {
765
+ if (e.code !== 'EEXIST') throw e;
766
+ }
755
767
  }
756
768
 
757
- // Create tsconfig if TypeScript
758
- if (useTypescript && !existsSync(join(cwd, 'tsconfig.json'))) {
769
+ // Create tsconfig if TypeScript (use wx flag to avoid TOCTOU race)
770
+ if (useTypescript) {
759
771
  const tsConfig = {
760
772
  compilerOptions: {
761
773
  target: 'ES2022',
@@ -777,13 +789,17 @@ export default defineConfig({
777
789
  exclude: ['node_modules', 'dist']
778
790
  };
779
791
 
780
- writeFileSync(join(cwd, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
781
- log.success('Created tsconfig.json');
792
+ try {
793
+ writeFileSync(join(cwd, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2), { flag: 'wx' });
794
+ log.success('Created tsconfig.json');
795
+ } catch (e) {
796
+ if (e.code !== 'EEXIST') throw e;
797
+ }
782
798
  }
783
799
 
784
800
  // Create index.html if it doesn't exist
785
801
  const indexHtmlPath = join(cwd, 'index.html');
786
- if (!existsSync(indexHtmlPath)) {
802
+ {
787
803
  const mainExt = useTypescript ? 'ts' : 'js';
788
804
  const indexHtml = `<!DOCTYPE html>
789
805
  <html lang="en">
@@ -798,14 +814,18 @@ export default defineConfig({
798
814
  </body>
799
815
  </html>
800
816
  `;
801
- writeFileSync(indexHtmlPath, indexHtml);
802
- log.success('Created index.html');
817
+ try {
818
+ writeFileSync(indexHtmlPath, indexHtml, { flag: 'wx' });
819
+ log.success('Created index.html');
820
+ } catch (e) {
821
+ if (e.code !== 'EEXIST') throw e;
822
+ }
803
823
  }
804
824
 
805
825
  // Create main entry file if it doesn't exist
806
826
  const mainExt = useTypescript ? 'ts' : 'js';
807
827
  const mainPath = join(cwd, 'src', `main.${mainExt}`);
808
- if (!existsSync(mainPath)) {
828
+ {
809
829
  const mainContent = useTypescript
810
830
  ? `import App from './App.pulse';
811
831
 
@@ -821,13 +841,17 @@ if (import.meta.hot) {
821
841
 
822
842
  App.mount('#app');
823
843
  `;
824
- writeFileSync(mainPath, mainContent);
825
- log.success(`Created src/main.${mainExt}`);
844
+ try {
845
+ writeFileSync(mainPath, mainContent, { flag: 'wx' });
846
+ log.success(`Created src/main.${mainExt}`);
847
+ } catch (e) {
848
+ if (e.code !== 'EEXIST') throw e;
849
+ }
826
850
  }
827
851
 
828
852
  // Create App.pulse if it doesn't exist
829
853
  const appPulsePath = join(cwd, 'src', 'App.pulse');
830
- if (!existsSync(appPulsePath)) {
854
+ {
831
855
  const appPulse = `@page App
832
856
 
833
857
  // Welcome to Pulse Framework!
@@ -970,20 +994,28 @@ style {
970
994
  }
971
995
  }
972
996
  `;
973
- writeFileSync(appPulsePath, appPulse);
974
- log.success('Created src/App.pulse');
997
+ try {
998
+ writeFileSync(appPulsePath, appPulse, { flag: 'wx' });
999
+ log.success('Created src/App.pulse');
1000
+ } catch (e) {
1001
+ if (e.code !== 'EEXIST') throw e;
1002
+ }
975
1003
  }
976
1004
 
977
1005
  // Create .gitignore if it doesn't exist
978
1006
  const gitignorePath = join(cwd, '.gitignore');
979
- if (!existsSync(gitignorePath)) {
1007
+ {
980
1008
  const gitignore = `node_modules
981
1009
  dist
982
1010
  .DS_Store
983
1011
  *.local
984
1012
  ${useTypescript ? '*.tsbuildinfo\n' : ''}`;
985
- writeFileSync(gitignorePath, gitignore);
986
- log.success('Created .gitignore');
1013
+ try {
1014
+ writeFileSync(gitignorePath, gitignore, { flag: 'wx' });
1015
+ log.success('Created .gitignore');
1016
+ } catch (e) {
1017
+ if (e.code !== 'EEXIST') throw e;
1018
+ }
987
1019
  }
988
1020
 
989
1021
  log.info(`
@@ -1283,9 +1315,7 @@ async function compileFiles(args) {
1283
1315
  if (result.success) {
1284
1316
  // Ensure output directory exists
1285
1317
  const outDir = dirname(outputFile);
1286
- if (!existsSync(outDir)) {
1287
- mkdirSync(outDir, { recursive: true });
1288
- }
1318
+ mkdirSync(outDir, { recursive: true });
1289
1319
  writeFileSync(outputFile, result.code);
1290
1320
  log.info(`Compiled: ${relPath} -> ${relOutput}`);
1291
1321
  writtenCount++;
package/cli/mobile.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Zero-dependency mobile platform for Pulse Framework
4
4
  */
5
5
 
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, readdirSync } from 'fs';
6
+ import { mkdirSync, readFileSync, writeFileSync, cpSync, readdirSync } from 'fs';
7
7
  import { join, dirname } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { execSync } from 'child_process';
@@ -19,7 +19,7 @@ const CONFIG_FILE = 'pulse.mobile.json';
19
19
  // ============================================================================
20
20
 
21
21
  /** Create directory if it doesn't exist */
22
- const mkdirp = (path) => !existsSync(path) && mkdirSync(path, { recursive: true });
22
+ const mkdirp = (path) => mkdirSync(path, { recursive: true });
23
23
 
24
24
  /** Create multiple directories at once */
25
25
  const mkdirs = (base, dirs) => dirs.forEach(d => mkdirp(join(base, d)));
@@ -83,24 +83,31 @@ async function initMobile(args) {
83
83
  console.log('Initializing Pulse Mobile...\n');
84
84
 
85
85
  // Check if dist exists
86
- if (!existsSync(join(root, 'dist'))) {
87
- console.warn('Warning: No dist/ folder found. Run "pulse build" first.\n');
86
+ try {
87
+ readdirSync(join(root, 'dist'));
88
+ } catch (e) {
89
+ if (e.code === 'ENOENT') {
90
+ console.warn('Warning: No dist/ folder found. Run "pulse build" first.\n');
91
+ } else {
92
+ throw e;
93
+ }
88
94
  }
89
95
 
90
96
  // Create mobile directory
91
- if (!existsSync(mobileDir)) {
92
- mkdirSync(mobileDir, { recursive: true });
93
- }
97
+ mkdirSync(mobileDir, { recursive: true });
94
98
 
95
99
  // Read project name from package.json
96
100
  let projectName = 'PulseApp';
97
101
  let packageId = 'com.pulse.app';
98
102
 
99
103
  const packageJsonPath = join(root, 'package.json');
100
- if (existsSync(packageJsonPath)) {
104
+ try {
101
105
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
102
106
  projectName = toPascalCase(pkg.name || 'PulseApp');
103
107
  packageId = `com.pulse.${pkg.name?.toLowerCase().replace(/[^a-z0-9]/g, '') || 'app'}`;
108
+ } catch (e) {
109
+ if (e.code !== 'ENOENT') throw e;
110
+ // No package.json, use defaults
104
111
  }
105
112
 
106
113
  // Create default config
@@ -122,54 +129,70 @@ async function initMobile(args) {
122
129
  };
123
130
 
124
131
  // Write config if not exists
125
- if (!existsSync(configPath)) {
126
- writeFileSync(configPath, JSON.stringify(config, null, 2));
132
+ try {
133
+ writeFileSync(configPath, JSON.stringify(config, null, 2), { flag: 'wx' });
127
134
  console.log(`Created ${CONFIG_FILE}`);
128
- } else {
129
- console.log(`${CONFIG_FILE} already exists, skipping...`);
135
+ } catch (err) {
136
+ if (err.code === 'EEXIST') {
137
+ console.log(`${CONFIG_FILE} already exists, skipping...`);
138
+ } else {
139
+ throw err;
140
+ }
130
141
  }
131
142
 
132
143
  // Copy Android template
133
144
  const androidTemplateDir = join(__dirname, '..', 'mobile', 'templates', 'android');
134
145
  const androidDir = join(mobileDir, 'android');
135
146
 
136
- if (!existsSync(androidDir)) {
137
- if (existsSync(androidTemplateDir)) {
147
+ try {
148
+ readdirSync(androidDir);
149
+ console.log('Android directory exists, skipping...');
150
+ } catch (e) {
151
+ if (e.code !== 'ENOENT') throw e;
152
+ try {
153
+ readdirSync(androidTemplateDir);
138
154
  console.log('Initializing Android project...');
139
155
  copyAndProcessTemplate(androidTemplateDir, androidDir, config);
140
156
  console.log('Android project created.');
141
- } else {
157
+ } catch (te) {
158
+ if (te.code !== 'ENOENT') throw te;
142
159
  console.log('Creating Android project structure...');
143
160
  createAndroidProject(androidDir, config);
144
161
  console.log('Android project created.');
145
162
  }
146
- } else {
147
- console.log('Android directory exists, skipping...');
148
163
  }
149
164
 
150
165
  // Copy iOS template
151
166
  const iosTemplateDir = join(__dirname, '..', 'mobile', 'templates', 'ios');
152
167
  const iosDir = join(mobileDir, 'ios');
153
168
 
154
- if (!existsSync(iosDir)) {
155
- if (existsSync(iosTemplateDir)) {
169
+ try {
170
+ readdirSync(iosDir);
171
+ console.log('iOS directory exists, skipping...');
172
+ } catch (e) {
173
+ if (e.code !== 'ENOENT') throw e;
174
+ try {
175
+ readdirSync(iosTemplateDir);
156
176
  console.log('Initializing iOS project...');
157
177
  copyAndProcessTemplate(iosTemplateDir, iosDir, config);
158
178
  console.log('iOS project created.');
159
- } else {
179
+ } catch (te) {
180
+ if (te.code !== 'ENOENT') throw te;
160
181
  console.log('Creating iOS project structure...');
161
182
  createIOSProject(iosDir, config);
162
183
  console.log('iOS project created.');
163
184
  }
164
- } else {
165
- console.log('iOS directory exists, skipping...');
166
185
  }
167
186
 
168
187
  // Copy bridge script to dist if it exists
169
188
  const bridgeSource = join(__dirname, '..', 'mobile', 'bridge', 'pulse-native.js');
170
- if (existsSync(join(root, 'dist')) && existsSync(bridgeSource)) {
189
+ try {
190
+ readdirSync(join(root, 'dist'));
171
191
  cpSync(bridgeSource, join(root, 'dist', 'pulse-native.js'));
172
192
  console.log('Native bridge script copied to dist/');
193
+ } catch (e) {
194
+ if (e.code !== 'ENOENT') throw e;
195
+ // dist or bridge source doesn't exist, skip
173
196
  }
174
197
 
175
198
  console.log(`
@@ -202,7 +225,10 @@ async function buildMobile(args) {
202
225
  const config = loadConfig(root);
203
226
 
204
227
  // First, ensure web build exists
205
- if (!existsSync(join(root, config.webDir))) {
228
+ try {
229
+ readdirSync(join(root, config.webDir));
230
+ } catch (e) {
231
+ if (e.code !== 'ENOENT') throw e;
206
232
  console.log('Building web app first...');
207
233
  const { buildProject } = await import('./build.js');
208
234
  await buildProject([]);
@@ -268,9 +294,14 @@ async function syncAssets(args) {
268
294
  async function syncWebAssets(root, platform, config) {
269
295
  const webDir = join(root, config.webDir);
270
296
 
271
- if (!existsSync(webDir)) {
272
- console.error(`Web directory "${config.webDir}" not found. Run "pulse build" first.`);
273
- process.exit(1);
297
+ try {
298
+ readdirSync(webDir);
299
+ } catch (e) {
300
+ if (e.code === 'ENOENT' || e.code === 'ENOTDIR') {
301
+ console.error(`Web directory "${config.webDir}" not found. Run "pulse build" first.`);
302
+ process.exit(1);
303
+ }
304
+ throw e;
274
305
  }
275
306
 
276
307
  let assetsDir;
@@ -288,8 +319,11 @@ async function syncWebAssets(root, platform, config) {
288
319
 
289
320
  // Copy native bridge
290
321
  const bridgeSource = join(__dirname, '..', 'mobile', 'bridge', 'pulse-native.js');
291
- if (existsSync(bridgeSource)) {
322
+ try {
292
323
  cpSync(bridgeSource, join(assetsDir, 'pulse-native.js'));
324
+ } catch (e) {
325
+ if (e.code !== 'ENOENT') throw e;
326
+ // Bridge source not found, skip
293
327
  }
294
328
 
295
329
  console.log(`Web assets synced to ${platform}`);
@@ -301,9 +335,14 @@ async function syncWebAssets(root, platform, config) {
301
335
  async function buildAndroid(root, config) {
302
336
  const androidDir = join(root, MOBILE_DIR, 'android');
303
337
 
304
- if (!existsSync(androidDir)) {
305
- console.error('Android project not found. Run "pulse mobile init" first.');
306
- process.exit(1);
338
+ try {
339
+ readdirSync(androidDir);
340
+ } catch (e) {
341
+ if (e.code === 'ENOENT' || e.code === 'ENOTDIR') {
342
+ console.error('Android project not found. Run "pulse mobile init" first.');
343
+ process.exit(1);
344
+ }
345
+ throw e;
307
346
  }
308
347
 
309
348
  console.log('Building Android APK...\n');
@@ -341,9 +380,14 @@ async function buildIOS(root, config) {
341
380
 
342
381
  const iosDir = join(root, MOBILE_DIR, 'ios');
343
382
 
344
- if (!existsSync(iosDir)) {
345
- console.error('iOS project not found. Run "pulse mobile init" first.');
346
- process.exit(1);
383
+ try {
384
+ readdirSync(iosDir);
385
+ } catch (e) {
386
+ if (e.code === 'ENOENT' || e.code === 'ENOTDIR') {
387
+ console.error('iOS project not found. Run "pulse mobile init" first.');
388
+ process.exit(1);
389
+ }
390
+ throw e;
347
391
  }
348
392
 
349
393
  console.log('Building iOS app...\n');
@@ -443,17 +487,25 @@ async function runIOS(root, config) {
443
487
  function loadConfig(root) {
444
488
  const configPath = join(root, CONFIG_FILE);
445
489
 
446
- if (!existsSync(configPath)) {
447
- console.error(`No ${CONFIG_FILE} found. Run "pulse mobile init" first.`);
448
- process.exit(1);
490
+ try {
491
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
492
+ } catch (e) {
493
+ if (e.code === 'ENOENT') {
494
+ console.error(`No ${CONFIG_FILE} found. Run "pulse mobile init" first.`);
495
+ process.exit(1);
496
+ }
497
+ throw e;
449
498
  }
450
-
451
- return JSON.parse(readFileSync(configPath, 'utf-8'));
452
499
  }
453
500
 
454
501
  /** Copy and process template files */
455
502
  function copyAndProcessTemplate(src, dest, config) {
456
- if (!existsSync(src)) return;
503
+ try {
504
+ readdirSync(src);
505
+ } catch (e) {
506
+ if (e.code === 'ENOENT') return;
507
+ throw e;
508
+ }
457
509
  mkdirp(dest);
458
510
 
459
511
  for (const file of readdirSync(src, { withFileTypes: true })) {