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 +1 -5
- package/index.js +5 -3
- package/package.json +1 -2
- package/src/spyne-file-prompt.js +3 -1
- package/src/spyne-starter-app-create.js +81 -10
- package/src/templates/generate-file-string.js +29 -55
- package/src/ui.js +1 -1
- package/src/utils/insert-channel-strings-to-index-file.js +119 -0
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
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/spyne-file-prompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
101
|
+
text: c.cyan('Cloning repository...'),
|
|
44
102
|
spinner: 'dots',
|
|
45
103
|
}).start();
|
|
46
104
|
|
|
47
105
|
try {
|
|
48
|
-
await git.clone(repoUrl, targetDir, [
|
|
49
|
-
|
|
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
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
48
|
-
import {Channel} from 'spyne';
|
|
49
|
-
|
|
50
|
-
export class ${props.className} extends Channel{
|
|
35
|
+
return `import { Channel } from 'spyne';
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|