@citolab/tspci 2.1.3 → 2.3.0-alpha.0
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/package.json +27 -45
- package/preset/base/global.d.ts +13 -0
- package/preset/base/package.json +22 -0
- package/preset/base/src/assets/procenten.png +0 -0
- package/preset/base/src/config.json +6 -0
- package/preset/base/src/index.ts +60 -0
- package/preset/base/src/styles.css +3 -0
- package/preset/base/tsconfig.json +19 -0
- package/preset/preact/src/index.tsx +66 -0
- package/preset/preact/src/interaction.tsx +36 -0
- package/preset/preact/src/store.ts +12 -0
- package/preset/tailwind/postcss.config.js +8 -0
- package/preset/tailwind/src/styles.css +7 -0
- package/preset/tailwind/tailwind.config.js +8 -0
- package/public/index.html +37 -8
- package/src/cli.js +308 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citolab/tspci",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0-alpha.0",
|
|
4
4
|
"description": "an opiniated typescript ims-pci builder with tao integration",
|
|
5
5
|
"author": "patrick <patrick.deklein@cito.nl>",
|
|
6
6
|
"repository": "@citolab/tspci",
|
|
@@ -9,51 +9,30 @@
|
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"types": "types/index.d.ts",
|
|
12
|
-
"devDependencies": {
|
|
13
|
-
"@rollup/plugin-alias": "^4.0.2",
|
|
14
|
-
"@rollup/plugin-babel": "^6.0.3",
|
|
15
|
-
"@rollup/plugin-commonjs": "^23.0.4",
|
|
16
|
-
"@rollup/plugin-image": "^3.0.1",
|
|
17
|
-
"@rollup/plugin-json": "^5.0.2",
|
|
18
|
-
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
19
|
-
"@rollup/plugin-replace": "^5.0.1",
|
|
20
|
-
"@rollup/plugin-terser": "^0.2.0",
|
|
21
|
-
"@rollup/plugin-typescript": "^10.0.1",
|
|
22
|
-
"@testing-library/dom": "^8.19.0",
|
|
23
|
-
"@types/node": "^18.11.12",
|
|
24
|
-
"@typescript-eslint/eslint-plugin": "^5.46.0",
|
|
25
|
-
"@typescript-eslint/parser": "^5.46.0",
|
|
26
|
-
"live-server": "^1.2.2",
|
|
27
|
-
"postcss": "^8.4.19",
|
|
28
|
-
"postcss-import": "^15.1.0",
|
|
29
|
-
"rollup": "^3.7.0",
|
|
30
|
-
"rollup-plugin-copy": "^3.4.0",
|
|
31
|
-
"rollup-plugin-live-server": "^2.0.0",
|
|
32
|
-
"rollup-plugin-node-builtins": "^2.1.2",
|
|
33
|
-
"rollup-plugin-node-globals": "^1.4.0",
|
|
34
|
-
"rollup-plugin-postcss": "^4.0.2",
|
|
35
|
-
"rollup-plugin-string": "^3.0.0",
|
|
36
|
-
"rollup-plugin-watch-assets": "^1.0.1",
|
|
37
|
-
"tailwindcss": "^3.2.4"
|
|
38
|
-
},
|
|
39
12
|
"dependencies": {
|
|
40
|
-
"@rollup/plugin-alias": "^
|
|
13
|
+
"@rollup/plugin-alias": "^5.0.0",
|
|
41
14
|
"@rollup/plugin-babel": "^6.0.3",
|
|
42
|
-
"@rollup/plugin-commonjs": "^
|
|
43
|
-
"@rollup/plugin-image": "^3.0.
|
|
44
|
-
"@rollup/plugin-json": "^
|
|
45
|
-
"@rollup/plugin-node-resolve": "^15.0.
|
|
46
|
-
"@rollup/plugin-replace": "^5.0.
|
|
47
|
-
"@rollup/plugin-terser": "^0.
|
|
48
|
-
"@rollup/plugin-typescript": "^
|
|
49
|
-
"@
|
|
50
|
-
"@
|
|
51
|
-
"@
|
|
52
|
-
"@typescript-eslint/
|
|
15
|
+
"@rollup/plugin-commonjs": "^24.1.0",
|
|
16
|
+
"@rollup/plugin-image": "^3.0.2",
|
|
17
|
+
"@rollup/plugin-json": "^6.0.0",
|
|
18
|
+
"@rollup/plugin-node-resolve": "^15.0.2",
|
|
19
|
+
"@rollup/plugin-replace": "^5.0.2",
|
|
20
|
+
"@rollup/plugin-terser": "^0.4.1",
|
|
21
|
+
"@rollup/plugin-typescript": "^11.1.0",
|
|
22
|
+
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
|
|
23
|
+
"@testing-library/dom": "^9.2.0",
|
|
24
|
+
"@types/node": "^18.16.3",
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
|
26
|
+
"@typescript-eslint/parser": "^5.59.2",
|
|
27
|
+
"autoprefixer": "10.4.14",
|
|
28
|
+
"chalk": "5.2.0",
|
|
29
|
+
"clear": "0.1.0",
|
|
30
|
+
"figlet": "1.6.0",
|
|
31
|
+
"inquirer": "9.2.0",
|
|
53
32
|
"live-server": "^1.2.2",
|
|
54
|
-
"postcss": "^8.4.
|
|
33
|
+
"postcss": "^8.4.23",
|
|
55
34
|
"postcss-import": "^15.1.0",
|
|
56
|
-
"rollup": "^3.
|
|
35
|
+
"rollup": "^3.21.4",
|
|
57
36
|
"rollup-plugin-copy": "^3.4.0",
|
|
58
37
|
"rollup-plugin-live-server": "^2.0.0",
|
|
59
38
|
"rollup-plugin-node-builtins": "^2.1.2",
|
|
@@ -61,8 +40,11 @@
|
|
|
61
40
|
"rollup-plugin-postcss": "^4.0.2",
|
|
62
41
|
"rollup-plugin-string": "^3.0.0",
|
|
63
42
|
"rollup-plugin-watch-assets": "^1.0.1",
|
|
64
|
-
"tailwindcss": "^3.2
|
|
43
|
+
"tailwindcss": "^3.3.2"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"help": "tspci --help",
|
|
47
|
+
"init": "tspci init --force --path=../../pcis"
|
|
65
48
|
},
|
|
66
|
-
"
|
|
67
|
-
"gitHead": "7cdbb1e4a3b177e3d8fc103679ecee6e65fdf79d"
|
|
49
|
+
"gitHead": "872d3b8628f1e7e9b2568563b8aae5ffa1a818d6"
|
|
68
50
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare module "*.css" {
|
|
2
|
+
const content: string;
|
|
3
|
+
export = content;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module "*.png" {
|
|
7
|
+
const content: string;
|
|
8
|
+
export = content;
|
|
9
|
+
}
|
|
10
|
+
declare module "qtiCustomInteractionContext" {
|
|
11
|
+
const register: {register: (PortableInteraction) => void};
|
|
12
|
+
export = register;
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-pci",
|
|
3
|
+
"description": "Here your PCI description",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/types.d.ts",
|
|
7
|
+
"private": true,
|
|
8
|
+
"dependencies": {},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@citolab/tspci": "^2.2.0"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tspci --dev",
|
|
14
|
+
"prod": "tspci"
|
|
15
|
+
},
|
|
16
|
+
"config": {
|
|
17
|
+
"tspci": {
|
|
18
|
+
"typeIdentifier": "myPci"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"source": "src/index.ts"
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Configuration, IMSpci, QtiVariableJSON } from "@citolab/tspci";
|
|
2
|
+
import * as ctx from "qtiCustomInteractionContext";
|
|
3
|
+
import configProps from "./config.json";
|
|
4
|
+
import style from "./styles.css"; // import and bundle this style file ( you can use tailwind and nested css )
|
|
5
|
+
import procenten from "./assets/procenten.png"; // image types are bundled inside the js
|
|
6
|
+
// Configuration
|
|
7
|
+
type PropTypes = typeof configProps;
|
|
8
|
+
|
|
9
|
+
class Pci implements IMSpci<PropTypes> {
|
|
10
|
+
typeIdentifier = "###___PCI_NAME___###"; // typeIdentifier is mandatory for all PCI's
|
|
11
|
+
config: Configuration<PropTypes>; // reference to the interface of the config object which you get when getInstance is called by the player
|
|
12
|
+
state: string; // keep a reference to the state
|
|
13
|
+
shadowdom: ShadowRoot | Element; // Not mandatory, but its wise to create a shadowroot
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
ctx && ctx.register(this); // we assume the qtiCustomInteractionContext is avaible due to the import above
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// First in the lifecycle of a PCI, this method is called with the domElement ( usually qti-interaction-markup ) where we can add our dom tree.
|
|
20
|
+
// config is the configuration object which has an onready
|
|
21
|
+
getInstance = (dom: HTMLElement, config: Configuration<any>, state: string) => {
|
|
22
|
+
config.properties = { ...configProps, ...config.properties }; // merge current props with incoming
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.state = state ? state : "";
|
|
25
|
+
|
|
26
|
+
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
27
|
+
this.render();
|
|
28
|
+
|
|
29
|
+
config.onready(this);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
private render = () => {
|
|
33
|
+
this.shadowdom.addEventListener("change", (e: Event) => {
|
|
34
|
+
const value = (e.target as HTMLInputElement).value;
|
|
35
|
+
this.state = value;
|
|
36
|
+
});
|
|
37
|
+
this.shadowdom.innerHTML = `<div class="pci-container">
|
|
38
|
+
<h1>${this.config.properties.title}</h1>
|
|
39
|
+
<div class="body">
|
|
40
|
+
<img width="${+this.config.properties.width}" height="${+this.config.properties.height}" src="${procenten}" />
|
|
41
|
+
</div>
|
|
42
|
+
<div class="interaction">
|
|
43
|
+
<label for="tentacles">${this.config.properties.prompt}</label>
|
|
44
|
+
<input type="number" value="${this.state}" min="0" max="100">%
|
|
45
|
+
</div>
|
|
46
|
+
</div>`;
|
|
47
|
+
const css = document.createElement("style");
|
|
48
|
+
css.innerHTML = style;
|
|
49
|
+
this.shadowdom.appendChild(css);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
getResponse = () => {
|
|
53
|
+
const response = (this.state === null || this.state === undefined) ? undefined : { base: { integer: this.state }} as QtiVariableJSON;
|
|
54
|
+
return response;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
getState = () => this.state.toString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default new Pci();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"lib": ["es6", "dom", "dom.iterable"],
|
|
5
|
+
"module": "es6",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"removeComments": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"strict": false,
|
|
10
|
+
"target": "es6",
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"jsxImportSource": "preact",
|
|
14
|
+
"allowSyntheticDefaultImports": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
},
|
|
18
|
+
"include": ["**/*"],
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "preact/debug";
|
|
2
|
+
import { render } from "preact";
|
|
3
|
+
import { initStore, Store } from "@citolab/preact-store";
|
|
4
|
+
|
|
5
|
+
import * as ctx from "qtiCustomInteractionContext";
|
|
6
|
+
import Interaction from "./interaction";
|
|
7
|
+
import style from "./styles.css";
|
|
8
|
+
import { Configuration, IMSpci, QtiVariableJSON } from "@citolab/tspci";
|
|
9
|
+
import configProps from "./config.json";
|
|
10
|
+
import { actions, StateModel } from "./store";
|
|
11
|
+
|
|
12
|
+
type PropTypes = typeof configProps;
|
|
13
|
+
|
|
14
|
+
class App implements IMSpci<PropTypes> {
|
|
15
|
+
typeIdentifier = "myPci"; // typeIdentifier is mandatory for all PCI's
|
|
16
|
+
config: Configuration<PropTypes>; // reference to the interface of the config object which you get when getInstance is called by the player
|
|
17
|
+
state: string; // keep a reference to the state
|
|
18
|
+
shadowdom: ShadowRoot; // Not mandatory, but its wise to create a shadowroot
|
|
19
|
+
store: Store<StateModel>;
|
|
20
|
+
|
|
21
|
+
private logActions: { type: string; payload: unknown }[] = []; // optional logActions
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
ctx && ctx.register(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getInstance = (dom: HTMLElement, config: Configuration<PropTypes>, stateString: string) => {
|
|
28
|
+
config.properties = { ...configProps, ...config.properties }; // merge current props with incoming
|
|
29
|
+
this.config = config;
|
|
30
|
+
|
|
31
|
+
const initState: StateModel = stateString ? JSON.parse(stateString).state : { input: 0 };
|
|
32
|
+
this.logActions = stateString ? JSON.parse(stateString).log : [];
|
|
33
|
+
this.store = initStore<StateModel>(actions as any[], initState, (action) => this.logActions.push(action));
|
|
34
|
+
|
|
35
|
+
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
36
|
+
this.render();
|
|
37
|
+
|
|
38
|
+
this.config.onready && this.config.onready(this);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
render = () => {
|
|
42
|
+
render(null, this.shadowdom);
|
|
43
|
+
const css = document.createElement("style");
|
|
44
|
+
css.innerHTML = style;
|
|
45
|
+
this.shadowdom.appendChild(css);
|
|
46
|
+
render(<Interaction config={this.config.properties} dom={this.shadowdom} />, this.shadowdom);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
getState = () =>
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
state: this.store.getState(),
|
|
52
|
+
log: this.logActions,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
getResponse = () => {
|
|
56
|
+
const state = this.store?.getState()?.input || undefined;
|
|
57
|
+
if (state === undefined) return undefined;
|
|
58
|
+
return {
|
|
59
|
+
base: {
|
|
60
|
+
integer: this.store.getState().input,
|
|
61
|
+
},
|
|
62
|
+
} as QtiVariableJSON;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default new App();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useStore } from "@citolab/preact-store";
|
|
2
|
+
import { ActionType, StateModel } from "./store";
|
|
3
|
+
import configProps from "./config.json";
|
|
4
|
+
import procenten from "./assets/procenten.png"; // image types are bundled inside the js
|
|
5
|
+
|
|
6
|
+
type PropTypes = typeof configProps;
|
|
7
|
+
|
|
8
|
+
const Interaction = ({ config, dom }: { config: PropTypes; dom: Document | ShadowRoot }) => {
|
|
9
|
+
const [state, dispatch] = useStore<StateModel, ActionType>(
|
|
10
|
+
(type, payload) => {
|
|
11
|
+
console.log(`type: ${type}, payload: ${JSON.stringify(payload)}`);
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
return <div className="pci-container">
|
|
15
|
+
<h1>{config.title}</h1>
|
|
16
|
+
<div className="body">
|
|
17
|
+
<img width={config.width} height={+config.height} src={procenten} />
|
|
18
|
+
</div>
|
|
19
|
+
<div className="interaction">
|
|
20
|
+
<label htmlFor="tentacles">{config.prompt}</label>
|
|
21
|
+
<input type="number"
|
|
22
|
+
onKeyUp={(e) => {
|
|
23
|
+
const input = e.target as HTMLInputElement;
|
|
24
|
+
dispatch<{ input: number }>("SET_INPUT", { input: +input.value });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
onChange={(e) => {
|
|
28
|
+
const input = e.target as HTMLInputElement;
|
|
29
|
+
dispatch<{ input: number }>("SET_INPUT", { input: +input.value });
|
|
30
|
+
}} min="0" max="100" />%
|
|
31
|
+
</div>
|
|
32
|
+
</div>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export default Interaction;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ActionType = "SET_INPUT";
|
|
2
|
+
|
|
3
|
+
export type StateModel = { input: number };
|
|
4
|
+
|
|
5
|
+
export const actions = [
|
|
6
|
+
{
|
|
7
|
+
type: "SET_INPUT" as ActionType,
|
|
8
|
+
action: (state: StateModel, { input }: { input: number }) => {
|
|
9
|
+
return {...state, input };
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
];
|
package/public/index.html
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
#pciContainer {
|
|
20
20
|
width: 100%;
|
|
21
21
|
/* min-width: 800px; */
|
|
22
|
+
|
|
22
23
|
max-width: 1024px;
|
|
23
24
|
height: 600px;
|
|
24
25
|
min-height: 600px;
|
|
@@ -47,7 +48,6 @@
|
|
|
47
48
|
user has not yet interacted yet, getResponse should return undefined.
|
|
48
49
|
</div>
|
|
49
50
|
|
|
50
|
-
|
|
51
51
|
<div id="pciContainer" class="shadow-sm">
|
|
52
52
|
<div id="loading" class="text-muted mx-auto p-4">Loading...</div>
|
|
53
53
|
</div>
|
|
@@ -124,6 +124,11 @@
|
|
|
124
124
|
location.reload();
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function playState() {
|
|
128
|
+
const state = item.getState();
|
|
129
|
+
item.store.replay(state.log); // state.log
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
function changeConfig(key, value) {
|
|
128
133
|
item.config.properties[key] = value;
|
|
129
134
|
item.trigger(`change${key}`, value);
|
|
@@ -210,20 +215,44 @@
|
|
|
210
215
|
<button class="btn btn-outline-secondary" onClick="${() => saveResponse()}">store</button>
|
|
211
216
|
<button class="btn btn-outline-secondary" onClick="${() => compareResponse()}">compare</button>
|
|
212
217
|
<button class="btn btn-outline-secondary" onClick="${() => setResponse()}">set</button>
|
|
213
|
-
<button
|
|
218
|
+
<button
|
|
219
|
+
class="btn btn-outline-secondary"
|
|
220
|
+
disabled="${!item.resetResponse}"
|
|
221
|
+
onClick="${() => resetResponse()}"
|
|
222
|
+
>
|
|
223
|
+
reset
|
|
224
|
+
</button>
|
|
214
225
|
</div>
|
|
215
226
|
</div>
|
|
216
227
|
|
|
217
228
|
<div>
|
|
218
229
|
<small class="text-muted">ㅤ</small>
|
|
219
230
|
<div class="d-flex">
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
<button
|
|
232
|
+
class="btn bg-light shadow-sm btn-outline-danger me-2"
|
|
233
|
+
disabled="${!(item.destroy || item.oncompleted)}"
|
|
234
|
+
onClick="${() => unload()}"
|
|
235
|
+
>
|
|
236
|
+
unload
|
|
237
|
+
</button>
|
|
238
|
+
<button class="btn bg-light shadow-sm btn-outline-danger" onClick="${() => reset()}">reset</button>
|
|
224
239
|
</div>
|
|
225
240
|
</div>
|
|
226
241
|
</div>
|
|
242
|
+
|
|
243
|
+
<div class="mt-2 d-flex gap-2">
|
|
244
|
+
<button class="btn btn-outline-secondary" onClick="${() => playState()}">play</button>
|
|
245
|
+
<input
|
|
246
|
+
class="form-control-range flex-grow-1"
|
|
247
|
+
type="range"
|
|
248
|
+
min="0"
|
|
249
|
+
max="${JSON.parse(item.getState()).log.length}"
|
|
250
|
+
value=${range}
|
|
251
|
+
onChange=${(e) => setRange(e.target.value)}
|
|
252
|
+
step="1"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
227
256
|
</div>
|
|
228
257
|
`;
|
|
229
258
|
}
|
|
@@ -267,8 +296,8 @@
|
|
|
267
296
|
window.onload = () => loadItem();
|
|
268
297
|
</script>
|
|
269
298
|
|
|
270
|
-
<h3 style="margin-top: 12rem
|
|
271
|
-
<ul style="margin-bottom:10rem
|
|
299
|
+
<h3 style="margin-top: 12rem">checklist</h3>
|
|
300
|
+
<ul style="margin-bottom: 10rem">
|
|
272
301
|
<li>Is the css scoped to the item?</li>
|
|
273
302
|
<li>Is the css/item somewhat responsive on smaller screens</li>
|
|
274
303
|
<li>Can you reset the item to its initial state</li>
|
package/src/cli.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadConfigFile } from "rollup/dist/loadConfigFile.js";
|
|
3
|
+
import { spawn } from "child_process";
|
|
3
4
|
import { rollup, watch } from "rollup";
|
|
5
|
+
import readline from "readline";
|
|
6
|
+
import chalk from "chalk";
|
|
4
7
|
import path from "path";
|
|
5
8
|
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import clear from "clear";
|
|
11
|
+
import figlet from "figlet";
|
|
12
|
+
import inquirer from "inquirer";
|
|
6
13
|
|
|
7
14
|
const args = process.argv.slice(2);
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
16
|
// PK: retreive dirname, most of the time automaticly inserted in es modules
|
|
12
17
|
import { fileURLToPath } from "url";
|
|
13
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -18,12 +23,12 @@ const PACKAGE_DIST_PATH = path.join(PACKAGE_ROOT_PATH, "dist");
|
|
|
18
23
|
const NODE_MODULES = path.join(process.cwd(), "node_modules");
|
|
19
24
|
|
|
20
25
|
// https://rollupjs.org/guide/en/#programmatically-loading-a-config-file
|
|
21
|
-
const watchBuild = (devServer = false, target =
|
|
26
|
+
const watchBuild = (devServer = false, target = "IMS") => {
|
|
22
27
|
const rollupFile = devServer ? "rollup.config.dev.js" : "rollup.config.js";
|
|
23
28
|
|
|
24
29
|
// PK: this ensures tailwind watches changes
|
|
25
30
|
// PK: See link here: https://github.com/tailwindlabs/tailwindcss/issues/7063
|
|
26
|
-
process.env.ROLLUP_WATCH = "true"
|
|
31
|
+
process.env.ROLLUP_WATCH = "true";
|
|
27
32
|
|
|
28
33
|
fs.rmSync(PACKAGE_DIST_PATH, { recursive: true, force: true });
|
|
29
34
|
!fs.existsSync(PACKAGE_DIST_PATH) && fs.mkdirSync(PACKAGE_DIST_PATH);
|
|
@@ -41,8 +46,8 @@ const watchBuild = (devServer = false, target = 'IMS') => {
|
|
|
41
46
|
);
|
|
42
47
|
} else {
|
|
43
48
|
let fileToCopy;
|
|
44
|
-
if (target ==
|
|
45
|
-
fileToCopy = path.join(__dirname, `../public/index.html`)
|
|
49
|
+
if (target == "IMS") {
|
|
50
|
+
fileToCopy = path.join(__dirname, `../public/index.html`);
|
|
46
51
|
} else {
|
|
47
52
|
fileToCopy = path.join(NODE_MODULES, `@citolab/tspci-${target}/public/index.html`);
|
|
48
53
|
}
|
|
@@ -100,17 +105,309 @@ const prodBuild = async () => {
|
|
|
100
105
|
});
|
|
101
106
|
};
|
|
102
107
|
|
|
108
|
+
function promptYesNo(prompt) {
|
|
109
|
+
const rl = readline.createInterface({
|
|
110
|
+
input: process.stdin,
|
|
111
|
+
output: process.stdout,
|
|
112
|
+
});
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
rl.question(`${prompt} (y/n): `, (answer) => {
|
|
115
|
+
const normalizedAnswer = answer.trim().toLowerCase();
|
|
116
|
+
if (normalizedAnswer === "y" || normalizedAnswer === "yes") {
|
|
117
|
+
console.log(chalk.green("Yes"));
|
|
118
|
+
resolve(true);
|
|
119
|
+
} else if (normalizedAnswer === "n" || normalizedAnswer === "no") {
|
|
120
|
+
console.log(chalk.red("No"));
|
|
121
|
+
resolve(false);
|
|
122
|
+
} else {
|
|
123
|
+
console.log("Invalid input. Please enter 'y' or 'n'.");
|
|
124
|
+
resolve(promptYesNo(prompt));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}).finally(() => {
|
|
128
|
+
rl.close();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function selectOptions(prompt, options) {
|
|
133
|
+
return inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: "checkbox",
|
|
136
|
+
message: prompt,
|
|
137
|
+
name: "values",
|
|
138
|
+
choices: options.map((o) => ({ name: o, checked: true })),
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function askOption(prompt) {
|
|
144
|
+
const rl = readline.createInterface({
|
|
145
|
+
input: process.stdin,
|
|
146
|
+
output: process.stdout,
|
|
147
|
+
});
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
rl.question(prompt, (option) => {
|
|
150
|
+
rl.close();
|
|
151
|
+
resolve(option);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createDirectoryIfNotExists(file) {
|
|
157
|
+
const dir = path.dirname(file);
|
|
158
|
+
if (!fs.existsSync(dir)) {
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function copyDir(presetFolder, destinationFolder) {
|
|
164
|
+
for (const fileOfDir of fs.readdirSync(presetFolder)) {
|
|
165
|
+
// check if object is a file or a folder
|
|
166
|
+
if (fs.statSync(path.join(presetFolder, fileOfDir)).isDirectory()) {
|
|
167
|
+
copyDir(path.join(presetFolder, fileOfDir), path.join(destinationFolder, fileOfDir));
|
|
168
|
+
} else {
|
|
169
|
+
const filename = fileOfDir;
|
|
170
|
+
const file = path.join(presetFolder, filename);
|
|
171
|
+
const newFileLocation = path.join(destinationFolder, filename);
|
|
172
|
+
createDirectoryIfNotExists(newFileLocation);
|
|
173
|
+
fs.copyFileSync(file, newFileLocation);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copyDirAndReplaceInFiles(presetFolder, destinationFolder, pciName) {
|
|
179
|
+
for (const fileOfDir of fs.readdirSync(presetFolder)) {
|
|
180
|
+
// check if object is a file or a folder
|
|
181
|
+
if (fs.statSync(path.join(presetFolder, fileOfDir)).isDirectory()) {
|
|
182
|
+
copyDirAndReplaceInFiles(path.join(presetFolder, fileOfDir), path.join(destinationFolder, fileOfDir), pciName);
|
|
183
|
+
} else {
|
|
184
|
+
const fileExtensionsToCheck = [".js", ".ts", ".tsx"];
|
|
185
|
+
const filename = fileOfDir;
|
|
186
|
+
const file = path.join(presetFolder, filename);
|
|
187
|
+
const fileExtension = path.extname(file);
|
|
188
|
+
const newFileLocation = path.join(destinationFolder, filename);
|
|
189
|
+
createDirectoryIfNotExists(newFileLocation);
|
|
190
|
+
if (fileExtensionsToCheck.includes(fileExtension)) {
|
|
191
|
+
// remove ###___name___### in the file content to the name
|
|
192
|
+
let content = fs.readFileSync(file, "utf8");
|
|
193
|
+
content = content.replace(/###___PCI_NAME___###/g, pciName);
|
|
194
|
+
// write newContent to newFileLocation
|
|
195
|
+
fs.writeFileSync(newFileLocation, content, "utf8");
|
|
196
|
+
} else {
|
|
197
|
+
fs.copyFileSync(file, path.join(destinationFolder, filename));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
103
203
|
const run = async () => {
|
|
104
204
|
const help = args.indexOf("--help") >= 0 || args.indexOf("-h") >= 0;
|
|
105
205
|
const watch = args.indexOf("--watch") >= 0 || args.indexOf("-w") >= 0;
|
|
106
206
|
const dev = args.indexOf("--dev") >= 0 || args.indexOf("-d") >= 0;
|
|
107
207
|
const target = args.indexOf("--target") >= 0 || args.indexOf("-t") >= 0;
|
|
208
|
+
const init = args.indexOf("init") >= 0;
|
|
108
209
|
|
|
109
210
|
if (help) {
|
|
110
|
-
console.log("--help -h list these commands
|
|
111
|
-
console.log("--build
|
|
112
|
-
console.log("--watch watch the files
|
|
113
|
-
console.log("--target build a prod item and bundle for platform
|
|
211
|
+
console.log(`•\t${chalk.blue("--help -h")} list these commands`);
|
|
212
|
+
console.log(`•\t${chalk.blue("--build")} build the project`);
|
|
213
|
+
console.log(`•\t${chalk.blue("--watch")} watch the files`);
|
|
214
|
+
console.log(`•\t${chalk.blue("--target")} build a prod item and bundle for platform`);
|
|
215
|
+
console.log("\n");
|
|
216
|
+
console.log(
|
|
217
|
+
`${chalk.blueBright("tspci init")} creates a new project \n•\t${chalk.blue(
|
|
218
|
+
"--force"
|
|
219
|
+
)} deletes the directory if it already exists \n•\t${chalk.blue(
|
|
220
|
+
"--no-install"
|
|
221
|
+
)} don't install dependencies \n•\t${chalk.blue("--path")} path to create the project in`
|
|
222
|
+
);
|
|
223
|
+
console.log(
|
|
224
|
+
`${chalk.blueBright("tspci add --target")} adds a new target to the project. E.g. tspci add --target=tao`
|
|
225
|
+
);
|
|
226
|
+
console.log("\n\n");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (init) {
|
|
231
|
+
clear();
|
|
232
|
+
console.log(chalk.green(figlet.textSync("@citolab/tspci", { horizontalLayout: "full" })));
|
|
233
|
+
|
|
234
|
+
// prompt for name
|
|
235
|
+
const pciName = (await askOption("What your pci identifier (typeIdentifier) (default: myPci) ")) || "myPci";
|
|
236
|
+
let destinationFolder = path.join(process.cwd(), pciName);
|
|
237
|
+
if (args.find((arg) => arg.startsWith("--path"))) {
|
|
238
|
+
const pathIndex = args.findIndex((arg) => arg.startsWith("--path"));
|
|
239
|
+
destinationFolder = path.join(process.cwd(), path.join(args[pathIndex].split("=")[1], pciName));
|
|
240
|
+
}
|
|
241
|
+
// if args contains --force then delete the directory if already exists
|
|
242
|
+
if (args.indexOf("--force") >= 0 || args.indexOf("-f") >= 0) {
|
|
243
|
+
if (fs.existsSync(destinationFolder)) {
|
|
244
|
+
console.log(chalk.yellow("Force deleting directory"));
|
|
245
|
+
fs.rmSync(destinationFolder, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// check if directory exists
|
|
249
|
+
while (pciName && fs.existsSync(destinationFolder)) {
|
|
250
|
+
console.log(
|
|
251
|
+
chalk.red(
|
|
252
|
+
"The directory already exists, please choose another name. Running tscpi init --force will delete the directory"
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// check if contains other characters than letters and numbers
|
|
258
|
+
if (!/^[a-zA-Z0-9]+$/.test(pciName)) {
|
|
259
|
+
console.log(
|
|
260
|
+
chalk.yellow("The identifier of your pci can only contain letters and numbers, stripping special characters")
|
|
261
|
+
);
|
|
262
|
+
pciName = pciName.replace(/[^a-zA-Z0-9]/g, "");
|
|
263
|
+
}
|
|
264
|
+
console.log(chalk.green(`pci: ${pciName}`));
|
|
265
|
+
const description = await askOption("What is the description of your pci? (default: pci created with tspci)") || "pci created with tspci";
|
|
266
|
+
|
|
267
|
+
const options = await selectOptions("Would you like to add other features?", ["tailwind css", "preact"]);
|
|
268
|
+
console.log(chalk.green(`Options: ${options.values.length === 0 ? "none" : options.values}`));
|
|
269
|
+
// get relative path to executable
|
|
270
|
+
const cliPath = __dirname;
|
|
271
|
+
|
|
272
|
+
// create the project folder
|
|
273
|
+
fs.mkdirSync(destinationFolder);
|
|
274
|
+
|
|
275
|
+
// copy the content of preset folder to the current folder
|
|
276
|
+
const presetFolder = path.resolve(cliPath, "./../preset/base");
|
|
277
|
+
if (!fs.existsSync(presetFolder)) {
|
|
278
|
+
console.log(chalk.red("Preset folder not found"));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(chalk.green("Copying files to PCI project folder"));
|
|
282
|
+
copyDirAndReplaceInFiles(presetFolder, destinationFolder, pciName);
|
|
283
|
+
const packageJson = path.join(destinationFolder, "package.json");
|
|
284
|
+
const packageJsonContent = fs.readFileSync(packageJson, "utf8");
|
|
285
|
+
const packageJsonContentJson = JSON.parse(packageJsonContent);
|
|
286
|
+
|
|
287
|
+
const kebabCasedProjectName = pciName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
288
|
+
packageJsonContentJson.name = kebabCasedProjectName;
|
|
289
|
+
packageJsonContentJson.description = description;
|
|
290
|
+
packageJsonContentJson.author = os?.userInfo()?.username;
|
|
291
|
+
if (!packageJsonContentJson.config) {
|
|
292
|
+
packageJsonContentJson.config = {};
|
|
293
|
+
}
|
|
294
|
+
if (!packageJsonContentJson.config.tspci) {
|
|
295
|
+
packageJsonContentJson.config.tspci = {};
|
|
296
|
+
}
|
|
297
|
+
packageJsonContentJson.config.tspci.typeIdentifier = pciName;
|
|
298
|
+
|
|
299
|
+
if (options.values.includes("tailwind css")) {
|
|
300
|
+
console.log(chalk.green("Adding tailwind css"));
|
|
301
|
+
|
|
302
|
+
// read postcss.config.js
|
|
303
|
+
const postcssConfig = path.resolve(cliPath, "./../preset/tailwind/postcss.config.js");
|
|
304
|
+
const postcssConfigContent = fs.readFileSync(postcssConfig, "utf8");
|
|
305
|
+
// get all plugin names
|
|
306
|
+
const pluginNames = postcssConfigContent
|
|
307
|
+
.match(/require\('(.*)'\)/g)
|
|
308
|
+
.map((plugin) => plugin.replace(/require\('(.*)'\)/, "$1"));
|
|
309
|
+
const packageJsonCli = path.resolve(cliPath, "./../package.json");
|
|
310
|
+
const packageJsonCliContent = fs.readFileSync(packageJsonCli, "utf8");
|
|
311
|
+
const packageJsonCliContentJson = JSON.parse(packageJsonCliContent);
|
|
312
|
+
// add all plugins to package.json and map the version to the version in tspci package.json
|
|
313
|
+
for (const pluginName of pluginNames) {
|
|
314
|
+
const pluginVersion = packageJsonCliContentJson.dependencies[pluginName];
|
|
315
|
+
packageJsonContentJson.devDependencies = {
|
|
316
|
+
...packageJsonContentJson.devDependencies,
|
|
317
|
+
[pluginName]: pluginVersion,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const newPackageJsonContent = JSON.stringify(packageJsonContentJson, null, 2);
|
|
321
|
+
fs.writeFileSync(packageJson, newPackageJsonContent);
|
|
322
|
+
// copy tailwind config file
|
|
323
|
+
copyDir(path.resolve(cliPath, "./../preset/tailwind"), destinationFolder);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (options.values.includes("preact")) {
|
|
327
|
+
console.log(chalk.green("Adding preact"));
|
|
328
|
+
const packageJson = path.join(destinationFolder, "package.json");
|
|
329
|
+
const packageJsonContent = fs.readFileSync(packageJson, "utf8");
|
|
330
|
+
let packageJsonContentJson = JSON.parse(packageJsonContent);
|
|
331
|
+
// delete index.ts
|
|
332
|
+
fs.rmSync(path.join(destinationFolder, "src/index.ts"), { force: true });
|
|
333
|
+
const preactPresetFolder = path.resolve(cliPath, "./../preset/preact");
|
|
334
|
+
copyDirAndReplaceInFiles(preactPresetFolder, destinationFolder, pciName);
|
|
335
|
+
// add preact and @citolab/preact-store to the package json
|
|
336
|
+
packageJsonContentJson.dependencies = {
|
|
337
|
+
...packageJsonContentJson.dependencies,
|
|
338
|
+
preact: "^10.13.2",
|
|
339
|
+
"@citolab/preact-store": "^2.0.9",
|
|
340
|
+
};
|
|
341
|
+
packageJsonContentJson = { ...packageJsonContentJson, source: "src/index.tsx" };
|
|
342
|
+
const newPackageJsonContent = JSON.stringify(packageJsonContentJson, null, 2);
|
|
343
|
+
fs.writeFileSync(packageJson, newPackageJsonContent);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// run npm instaal in destination folder
|
|
347
|
+
if (args.indexOf("--no-install") === -1) {
|
|
348
|
+
console.log(chalk.green("Installing dependencies"));
|
|
349
|
+
const npm = spawn("npm", ["install"], { cwd: destinationFolder });
|
|
350
|
+
npm.stdout.on("data", (data) => {
|
|
351
|
+
console.log(chalk.green(data.toString()));
|
|
352
|
+
console.log(chalk.green("Done! You can now start the dev server with npm run dev"));
|
|
353
|
+
|
|
354
|
+
// console.log(chalk.blue("Starting dev server..."));
|
|
355
|
+
// // run npm run dev in destination folder
|
|
356
|
+
// const npm = spawn("npm", ["run", "dev"], { cwd: destinationFolder });
|
|
357
|
+
// npm.stdout.on("data", (data) => {
|
|
358
|
+
// console.log(chalk.blue(data.toString()));
|
|
359
|
+
// });
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
} else {
|
|
363
|
+
console.log(chalk.yellow("Skipping npm install, run npm install in the project folder to install dependencies"));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (args.indexOf("add") !== -1) {
|
|
368
|
+
const targetIndex = args.indexOf("--target");
|
|
369
|
+
if (args.length <= targetIndex + 1) {
|
|
370
|
+
console.error("Missing target name. Use tspci add --target target");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const targetname = args[targetIndex + 1];
|
|
374
|
+
const packageFileContent = fs.readFileSync(path.join(PACKAGE_ROOT_PATH, "package.json"), "utf8");
|
|
375
|
+
const packageJson = JSON.parse(packageFileContent);
|
|
376
|
+
|
|
377
|
+
const modifyPci = () => {
|
|
378
|
+
const scriptFile = path.join(path.join(path.join(path.join(NODE_MODULES, `@citolab`), `tspci-${targetname}`), 'src'), `init.mjs`);
|
|
379
|
+
let init = false;
|
|
380
|
+
if (fs.existsSync(scriptFile)) {
|
|
381
|
+
// run init() script
|
|
382
|
+
import(`file:///${scriptFile}`).then((script) => {
|
|
383
|
+
if (script.init) {
|
|
384
|
+
init = true;
|
|
385
|
+
console.log(chalk.green(`Running init script for @citolab/tspci-${targetname}`));
|
|
386
|
+
script.init();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (!init) {
|
|
391
|
+
console.log(chalk.green(`No init script found for @citolab/tspci-${targetname}`));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// check if `@citolab/tspci-${targetname}]}` is already in devDependencies
|
|
396
|
+
if (!packageJson.devDependencies || !packageJson.devDependencies[`@citolab/tspci-${targetname}`]) {
|
|
397
|
+
console.log(chalk.white(`Adding @citolab/tspci-${targetname}`));
|
|
398
|
+
// install @citolab/tspci-${targetname} to devDepencies
|
|
399
|
+
const npm = spawn("npm", ["install", `@citolab/tspci-${targetname}`, "--save-dev"], { cwd: process.cwd() });
|
|
400
|
+
// wait till npm install is done
|
|
401
|
+
npm.stdout.on("data", (data) => {
|
|
402
|
+
console.log(chalk.green(`successfull installed @citolab/tspci-${targetname}`));
|
|
403
|
+
// run init script if available
|
|
404
|
+
modifyPci();
|
|
405
|
+
});
|
|
406
|
+
} else {
|
|
407
|
+
modifyPci();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
114
411
|
return;
|
|
115
412
|
}
|
|
116
413
|
|
|
@@ -138,7 +435,7 @@ const run = async () => {
|
|
|
138
435
|
console.error("Missing target name. Use tspci --target target");
|
|
139
436
|
}
|
|
140
437
|
const targetname = args[targetIndex + 1];
|
|
141
|
-
const scriptFile = path.join(NODE_MODULES, `@citolab
|
|
438
|
+
const scriptFile = path.join(path.join(path.join(path.join(NODE_MODULES, `@citolab`), `tspci-${targetname}`), 'src'), `createPackage.mjs`);
|
|
142
439
|
import(`file:///${scriptFile}`).then((script) => {
|
|
143
440
|
script.bundle();
|
|
144
441
|
});
|