spyne-cli 0.6.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,10 +16,9 @@ To install the `spyne-cli`, you need to have Node.js and npm installed on your s
16
16
  1. Install the package globally:
17
17
 
18
18
  ```bash
19
- npm install -g @spynejs/spyne-cli
19
+ npm install -g spyne-cli
20
20
  ```
21
21
 
22
-
23
22
  ## Usage
24
23
 
25
24
  ### Creating a New SpyneJS Application
@@ -100,6 +99,3 @@ Contributions are welcome! Please feel free to submit a Pull Request or open an
100
99
  ## License
101
100
 
102
101
  This project is licensed under the MIT License.
103
- ```
104
-
105
- You can now paste this content directly into your `README.md` file. Let me know if you need further assistance!
package/index.js CHANGED
@@ -8,6 +8,9 @@ import SpyneFilePrompt from './src/spyne-file-prompt.js';
8
8
  const command = process.argv[2];
9
9
  const appName = process.argv[3];
10
10
 
11
+ // Simple check if user included "-spa" or "--spa" anywhere in the process.argv
12
+ const isSPA = process.argv.includes('-spa') || process.argv.includes('--spa');
13
+
11
14
  const startPromptFn = async () => {
12
15
  clear();
13
16
  SpyneCliUI.title();
@@ -16,11 +19,10 @@ const startPromptFn = async () => {
16
19
  };
17
20
 
18
21
  if (command === 'new' && appName) {
19
- // We can use an immediately-invoked async function
20
- // OR top-level await if your Node version supports it.
21
22
  (async () => {
22
23
  try {
23
- await createNewApp(appName);
24
+ // Pass the isSPA boolean to createNewApp
25
+ await createNewApp(appName, isSPA);
24
26
  } catch (err) {
25
27
  console.error('Failed to create new application:', err.message);
26
28
  process.exit(1);
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "spyne-cli": "index.js"
5
5
  },
6
6
  "type": "module",
7
- "version": "0.6.0",
7
+ "version": "0.6.5",
8
8
  "description": "Generates spyne objects and saves them to standard spyne.",
9
9
  "main": "index.js",
10
10
  "scripts": {
@@ -35,7 +35,6 @@
35
35
  "ora": "^8.1.1",
36
36
  "package-up": "^5.0.0",
37
37
  "pkg-dir": "^8.0.0",
38
- "pkg-up": "^5.0.0",
39
38
  "ramda": "^0.30.1",
40
39
  "read-pkg": "^9.0.1",
41
40
  "recast": "^0.23.9",
@@ -8,6 +8,7 @@ import GenerateFileString from './templates/generate-file-string.js';
8
8
  import {onSaveSpyneFileToDir} from './utils/file-utils.js';
9
9
  import {generatePromptOutput} from './templates/generate-prompt-output.js';
10
10
  import {addChannelToIndexJS} from './utils/add-channel-to-index-file.js';
11
+ import {insertChannelStrings} from './utils/insert-channel-strings-to-index-file.js';
11
12
 
12
13
  export default class SpyneFilePrompt {
13
14
 
@@ -46,7 +47,8 @@ export default class SpyneFilePrompt {
46
47
  let channelHasRegistered = false;
47
48
 
48
49
  if (fileType === 'Channel'){
49
- const savedChannelToIndexProps = addChannelToIndexJS(className, fileName);
50
+ //const savedChannelToIndexProps = addChannelToIndexJS(className, fileName);
51
+ const savedChannelToIndexProps = insertChannelStrings(className, fileName);
50
52
 
51
53
  // check if channel has been registered
52
54
  channelHasRegistered = savedChannelToIndexProps.fileHasSaved;
@@ -12,10 +12,8 @@ async function installDependencies(appName) {
12
12
  }).start();
13
13
 
14
14
  return new Promise((resolve, reject) => {
15
- // Use --silent to suppress npm logs
16
15
  const child = spawn('npm', ['install', '--silent'], {
17
16
  cwd: appName,
18
- // stdio: 'ignore' → all output is ignored, the spinner continues unblocked
19
17
  stdio: 'ignore',
20
18
  });
21
19
 
@@ -31,28 +29,93 @@ async function installDependencies(appName) {
31
29
  });
32
30
  }
33
31
 
34
- export async function createNewApp(appName) {
32
+ /**
33
+ * Update <title> in index.tmpl.html
34
+ * @param {string} targetDir
35
+ * @param {string} appName
36
+ */
37
+ function updateIndexTitle(targetDir, appName) {
38
+ const indexFilePath = path.join(targetDir, 'src', 'index.tmpl.html');
39
+ if (!fs.existsSync(indexFilePath)) return; // skip if not found
40
+
41
+ let content = fs.readFileSync(indexFilePath, 'utf-8');
42
+ // Replace first occurrence of <title>...</title>
43
+ content = content.replace(/<title>.*<\/title>/, `<title>${appName}</title>`);
44
+
45
+ fs.writeFileSync(indexFilePath, content, 'utf-8');
46
+ }
47
+
48
+ /**
49
+ * Update "name" field in package.json
50
+ * @param {string} targetDir
51
+ * @param {string} appName
52
+ */
53
+ function updatePackageName(targetDir, appName) {
54
+ const pkgPath = path.join(targetDir, 'package.json');
55
+ if (!fs.existsSync(pkgPath)) return; // skip if not found
56
+
57
+ const pkgData = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
58
+
59
+ // sanitize: e.g. convert to lowercase, replace spaces with dashes
60
+ const sanitizedName = appName
61
+ .toLowerCase()
62
+ .replace(/\s+/g, '-')
63
+ .replace(/[^a-z0-9-_]/g, ''); // remove any non-alphanumeric (optional)
64
+
65
+ pkgData.name = sanitizedName || 'my-spyne-app';
66
+
67
+ fs.writeFileSync(pkgPath, JSON.stringify(pkgData, null, 2), 'utf-8');
68
+ }
69
+
70
+ /**
71
+ * Update the project details (title, pkg name)
72
+ */
73
+ function updateProjectDetails(targetDir, appName) {
74
+ updateIndexTitle(targetDir, appName);
75
+ updatePackageName(targetDir, appName);
76
+ }
77
+
78
+ /**
79
+ * Creates a new Spyne app in the directory `appName`.
80
+ * If isSPA is true, clones the single-page-app template (spa-base-alpha).
81
+ * Otherwise, clones the default starter-app-alpha.
82
+ *
83
+ * @param {string} appName - Name of the app / folder
84
+ * @param {boolean} isSPA - Whether to clone the SPA repo
85
+ */
86
+ export async function createNewApp(appName, isSPA = false) {
35
87
  const targetDir = path.resolve(process.cwd(), appName);
36
- const repoUrl = 'https://github.com/nybatista/starter-app-alpha.git';
88
+
89
+ // Choose repo based on isSPA flag
90
+ const repoUrl = isSPA
91
+ ? 'https://github.com/nybatista/spa-base-alpha.git'
92
+ : 'https://github.com/nybatista/starter-app-alpha.git';
93
+
37
94
  const git = simpleGit();
38
95
 
39
96
  console.log(c.cyan(`\nCreating a new Spyne app in ${c.bold(appName)}...`));
97
+ console.log(c.yellow(`Using repo: ${repoUrl}`));
40
98
 
41
99
  // 1) Clone repository with Ora spinner
42
100
  let spinner = ora({
43
- text: c.cyan('Cloning starter-app repository...'),
101
+ text: c.cyan('Cloning repository...'),
44
102
  spinner: 'dots',
45
103
  }).start();
46
104
 
47
105
  try {
48
- await git.clone(repoUrl, targetDir, ['--depth=1']);
49
- spinner.succeed(c.greenBright('Starter App cloned successfully!'));
106
+ await git.clone(repoUrl, targetDir, [
107
+ '--branch=main',
108
+ '--single-branch',
109
+ '--depth=1'
110
+ ]);
111
+
112
+ spinner.succeed(c.greenBright('Repository cloned successfully!'));
50
113
  } catch (err) {
51
114
  spinner.fail(c.red(`Failed to clone the repository: ${err.message}`));
52
115
  process.exit(1);
53
116
  }
54
117
 
55
- // 2) Remove .git (silent - no user message)
118
+ // 2) Remove .git
56
119
  try {
57
120
  fs.rmSync(path.join(targetDir, '.git'), { recursive: true, force: true });
58
121
  } catch (err) {
@@ -60,7 +123,15 @@ export async function createNewApp(appName) {
60
123
  process.exit(1);
61
124
  }
62
125
 
63
- // 3) Asynchronous install (silent)
126
+ // 3) Update index.tmpl.html <title> and package.json name
127
+ try {
128
+ updateProjectDetails(targetDir, appName);
129
+ } catch (err) {
130
+ console.error(c.red(`Failed to update project details: ${err.message}`));
131
+ // not a fatal error in most cases, but you can decide to exit if needed
132
+ }
133
+
134
+ // 4) Install dependencies
64
135
  try {
65
136
  await installDependencies(appName);
66
137
  } catch (err) {
@@ -68,7 +139,7 @@ export async function createNewApp(appName) {
68
139
  process.exit(1);
69
140
  }
70
141
 
71
- // 4) Final success message
142
+ // 5) Final message
72
143
  console.log(`\n${c.greenBright('Success!')}`);
73
144
  console.log(c.cyan(`Created ${c.bold(appName)} at ${targetDir}\n`));
74
145
  console.log(c.greenBright('Next steps:'));
@@ -1,96 +1,70 @@
1
1
  const _viewStreamTemplate = (props)=> {
2
- return `import {ViewStream} from 'spyne';
2
+ return `import { ViewStream } from 'spyne';
3
3
 
4
4
  export class ${props.className} extends ViewStream {
5
-
6
- constructor(props={}) {
7
-
8
- super(props);
9
- }
10
-
11
- addActionListeners() {
12
- // return nested array(s)
13
- return [];
14
- }
15
-
16
- broadcastEvents() {
17
- // return nested array(s)
18
- return [];
19
- }
20
-
21
- onRendered() {
22
-
23
- }
24
-
25
- }
5
+ constructor(props = {}) {
6
+ super(props);
7
+ }
8
+
9
+ addActionListeners() {
10
+ return [];
11
+ }
26
12
 
13
+ broadcastEvents() {
14
+ return [];
15
+ }
16
+
17
+ onRendered() {}
18
+ }
27
19
  `
28
20
  }
29
21
 
30
22
  const _domElementTemplate = (props) => {
31
- return `import {DomElement} from 'spyne';
32
-
23
+ return `import { DomElement } from 'spyne';
24
+
33
25
  export class ${props.className} extends DomElement {
34
-
35
- constructor(props={}) {
36
-
26
+ constructor(props = {}) {
37
27
  super(props);
38
28
  }
39
-
40
29
  }
41
-
42
30
  `
43
31
  }
44
32
 
45
33
 
46
34
  const _channelTemplate = (props)=> {
47
- return `import {Subject} from 'rxjs';
48
- import {Channel} from 'spyne';
49
-
50
- export class ${props.className} extends Channel{
35
+ return `import { Channel } from 'spyne';
51
36
 
52
- constructor(name, props={}) {
53
- name="${props.channelName}";
37
+ export class ${props.className} extends Channel {
38
+ constructor(name, props = {}) {
39
+ name = '${props.channelName}';
54
40
  props.sendCachedPayload = ${props.replayLastPayload};
55
-
56
41
  super(name, props);
57
42
  }
58
43
 
59
- onRegistered(){
60
-
61
- }
44
+ onRegistered() {}
62
45
 
63
46
  addRegisteredActions() {
64
-
65
47
  return [];
66
48
  }
67
49
 
68
- onViewStreamInfo(obj) {
69
- let data = obj.props();
70
- }
71
-
50
+ onViewStreamInfo() {}
72
51
  }
73
-
74
52
  `
75
53
  }
76
54
 
77
55
  const _spyneTraitTemplate = (props)=>{
78
- return `import {SpyneTrait} from 'spyne';
56
+ return `import { SpyneTrait } from 'spyne';
79
57
 
80
58
  export class ${props.className} extends SpyneTrait {
81
-
82
- constructor(context){
83
- let traitPrefix = "${props.methodPrefix}";
84
-
59
+ constructor(context) {
60
+ let traitPrefix = '${props.methodPrefix}';
85
61
  super(context, traitPrefix);
86
62
  }
87
-
88
- static ${props.methodPrefix}HelloWorld(){
89
- return "Hello World";
63
+
64
+ static ${props.methodPrefix}HelloWorld() {
65
+ return 'Hello World';
90
66
  }
91
-
92
67
  }
93
-
94
68
  `
95
69
  }
96
70
 
package/src/ui.js CHANGED
@@ -5,7 +5,7 @@ export class SpyneCliUI {
5
5
 
6
6
  constructor() {}
7
7
  static title(){
8
- const figletTxt = figlet.textSync('spyne-cli 6.0', {
8
+ const figletTxt = figlet.textSync('spyne-cli 6.5', {
9
9
  horizontalLayout: 'universal smushing'
10
10
  })
11
11
  const chalkOutput = chalk.blue(figletTxt);
@@ -0,0 +1,119 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import c from 'ansi-colors';
4
+ //import {Data} from '../spyne-template-prompts.js';
5
+ //const {appPath} = Data;
6
+
7
+ export function insertChannelStrings(channelClassName, channelFileName, indexJsPath="./src/index.js" ) {
8
+ //console.log("INFO IS ",{appPath, channelClassName, channelFileName, indexJsPath})
9
+ try {
10
+ if (!fs.existsSync(indexJsPath)) {
11
+ console.log(c.red(`Error: ./src/index.js not found at ${indexJsPath}`));
12
+ return;
13
+ }
14
+
15
+ let fileContent = fs.readFileSync(indexJsPath, 'utf-8');
16
+ let lines = fileContent.split('\n');
17
+
18
+ // 1) Find the last import line
19
+ let lastImportIndex = -1;
20
+ for (let i = 0; i < lines.length; i++) {
21
+ // A naive check: line starts with "import "
22
+ // or you can also check .match(/^import\s+/)
23
+ if (lines[i].trim().startsWith('import ')) {
24
+ lastImportIndex = i;
25
+ }
26
+ }
27
+
28
+ // 2) Find the line with SpyneApp.init(...) or spyneApp.init(...)
29
+ // We'll store the first occurrence
30
+ let initIndex = -1;
31
+ const initRegex = /(SpyneApp\.init\s*\()|(spyneApp\.init\s*\()/;
32
+ for (let i = 0; i < lines.length; i++) {
33
+ if (initRegex.test(lines[i])) {
34
+ initIndex = i;
35
+ break;
36
+ }
37
+ }
38
+
39
+ // 3) Build the import snippet
40
+ // e.g. "import {ChannelTest_1} from 'channels/channel-test-1.js';"
41
+ const importSnippet = `import { ${channelClassName} } from 'channels/${channelFileName}';`;
42
+
43
+ // 4) Decide where to insert the import line
44
+ // - Must be AFTER the last import
45
+ // - Must be BEFORE the init line (if init line is after the last import)
46
+ // If init line doesn't exist or it's above the last import, we'll just place after last import.
47
+ let importInsertionIndex = lastImportIndex + 1;
48
+ if (initIndex !== -1 && initIndex > lastImportIndex) {
49
+ // If there's space to place it exactly before the init line
50
+ // we'll insert at the init line index. That effectively
51
+ // pushes the init line down by 1 line.
52
+ importInsertionIndex = Math.min(initIndex, lastImportIndex + 1);
53
+ }
54
+
55
+ // Insert the import line
56
+ lines.splice(importInsertionIndex, 0, importSnippet);
57
+
58
+ // 5) Build the registerChannel snippet
59
+ // e.g. "SpyneApp.registerChannel(new ChannelTest_1());"
60
+ const registerSnippet = `SpyneApp.registerChannel(new ${channelClassName}());`;
61
+
62
+ // 6) Insert the register snippet right AFTER the init line, if found
63
+ if (initIndex !== -1) {
64
+ // Because we inserted one line before the init,
65
+ // the init line is now at initIndex+1, so the place to insert
66
+ // is initIndex+2. But let's re-scan for the init line to be safe,
67
+ // or simply adjust.
68
+ let updatedInitIndex = -1;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ if (initRegex.test(lines[i])) {
71
+ updatedInitIndex = i;
72
+ break;
73
+ }
74
+ }
75
+
76
+ if (updatedInitIndex !== -1) {
77
+ lines.splice(updatedInitIndex + 1, 0, registerSnippet);
78
+ } else {
79
+ // If we somehow lost the init line, fallback to end
80
+ lines.push(registerSnippet);
81
+ }
82
+ } else {
83
+ // No init line found, let's just push it at the end or skip
84
+ console.log(c.yellow('Warning: SpyneApp.init(...) not found. Skipping registerChannel insertion.'));
85
+ }
86
+
87
+ // 7) Write the updated content back
88
+ fs.writeFileSync(indexJsPath, lines.join('\n'), 'utf-8');
89
+ //console.log(c.green(`✔ Successfully updated ./src/index.js to import and register "${channelClassName}".`));
90
+
91
+ return {
92
+ fileHasSaved: true,
93
+ errorType: null
94
+ };
95
+
96
+
97
+ } catch (error) {
98
+ let errorType = 'UnknownError';
99
+ if (error.code === 'ENOENT') {
100
+ errorType = 'FileNotFound';
101
+ } else if (error.name === 'SyntaxError') {
102
+ errorType = 'SyntaxError';
103
+ } else if (error.name === 'TypeError') {
104
+ errorType = 'TypeError';
105
+ } else if (error.name === 'PermissionError') {
106
+ errorType = 'PermissionError';
107
+ }
108
+ //console.warn(`spyne-cli register channel, ${errorType} incomplete `,error)
109
+
110
+ // Fail silently and return error information
111
+ return {
112
+ fileHasSaved: false,
113
+ errorType
114
+ };
115
+
116
+
117
+ //console.error(c.red(`Error updating index.js: ${err.message}`));
118
+ }
119
+ }