@learnpack/learnpack 5.0.53 → 5.0.57
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 +30 -12
- package/lib/commands/publish.js +29 -7
- package/lib/commands/serve.d.ts +7 -0
- package/lib/commands/serve.js +149 -0
- package/lib/managers/server/routes.js +2 -0
- package/lib/utils/api.d.ts +4 -0
- package/lib/utils/api.js +21 -4
- package/lib/utils/cloudStorage.d.ts +8 -0
- package/lib/utils/cloudStorage.js +17 -0
- package/oclif.manifest.json +1 -1
- package/package.json +3 -1
- package/src/commands/publish.ts +68 -12
- package/src/commands/serve.ts +192 -0
- package/src/creator/README.md +54 -0
- package/src/creator/eslint.config.js +28 -0
- package/src/creator/index.html +13 -0
- package/src/creator/package-lock.json +4659 -0
- package/src/creator/package.json +41 -0
- package/src/creator/public/vite.svg +1 -0
- package/src/creator/src/App.css +42 -0
- package/src/creator/src/App.tsx +221 -0
- package/src/creator/src/assets/react.svg +1 -0
- package/src/creator/src/assets/svgs.tsx +88 -0
- package/src/creator/src/components/Loader.tsx +28 -0
- package/src/creator/src/components/Login.tsx +263 -0
- package/src/creator/src/components/SelectableCard.tsx +30 -0
- package/src/creator/src/components/StepWizard.tsx +77 -0
- package/src/creator/src/components/SyllabusEditor.tsx +431 -0
- package/src/creator/src/index.css +68 -0
- package/src/creator/src/main.tsx +19 -0
- package/src/creator/src/utils/configTypes.ts +122 -0
- package/src/creator/src/utils/constants.ts +2 -0
- package/src/creator/src/utils/lib.ts +36 -0
- package/src/creator/src/utils/rigo.ts +391 -0
- package/src/creator/src/utils/store.ts +78 -0
- package/src/creator/src/vite-env.d.ts +1 -0
- package/src/creator/tsconfig.app.json +26 -0
- package/src/creator/tsconfig.json +7 -0
- package/src/creator/tsconfig.node.json +24 -0
- package/src/creator/vite.config.ts +13 -0
- package/src/creatorDist/assets/index-D92OoEoU.js +23719 -0
- package/src/creatorDist/assets/index-tt9JBVY0.css +987 -0
- package/src/creatorDist/index.html +14 -0
- package/src/creatorDist/vite.svg +1 -0
- package/src/managers/server/routes.ts +3 -0
- package/src/ui/_app/app.css +1 -0
- package/src/ui/_app/app.js +3025 -0
- package/src/ui/_app/favicon.ico +0 -0
- package/src/ui/_app/index.html +109 -0
- package/src/ui/_app/index.html.backup +91 -0
- package/src/ui/_app/learnpack.svg +7 -0
- package/src/ui/_app/logo-192.png +0 -0
- package/src/ui/_app/logo-512.png +0 -0
- package/src/ui/_app/logo.png +0 -0
- package/src/ui/_app/manifest.webmanifest +21 -0
- package/src/ui/_app/sw.js +30 -0
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +24 -4
- package/src/utils/cloudStorage.ts +24 -0
- package/src/utils/creds.json +13 -0
package/src/commands/publish.ts
CHANGED
@@ -26,13 +26,15 @@ const handleAssetCreation = async (
|
|
26
26
|
sessionPayload: any,
|
27
27
|
academy: TAcademy,
|
28
28
|
learnJson: any,
|
29
|
-
learnpackDeployUrl: string
|
29
|
+
learnpackDeployUrl: string,
|
30
|
+
category: number
|
30
31
|
) => {
|
31
32
|
try {
|
32
33
|
const { exists, academyId } = await api.doesAssetExists(
|
33
34
|
sessionPayload.token,
|
34
35
|
learnJson.slug
|
35
36
|
)
|
37
|
+
|
36
38
|
if (!exists) {
|
37
39
|
Console.info("Asset does not exist in this academy, creating it")
|
38
40
|
const asset = await api.createAsset(sessionPayload.token, academy.id, {
|
@@ -43,6 +45,9 @@ const handleAssetCreation = async (
|
|
43
45
|
learnpack_deploy_url: learnpackDeployUrl,
|
44
46
|
technologies: ["node", "bash"],
|
45
47
|
url: "https://4geeksacademy.com",
|
48
|
+
category: category,
|
49
|
+
owner: sessionPayload.id,
|
50
|
+
author: sessionPayload.id,
|
46
51
|
})
|
47
52
|
Console.info("Asset created with id", asset.id)
|
48
53
|
} else {
|
@@ -90,13 +95,38 @@ const runAudit = (strict: boolean) => {
|
|
90
95
|
)
|
91
96
|
}
|
92
97
|
|
93
|
-
|
98
|
+
type Academy = {
|
99
|
+
id: number
|
100
|
+
name: string
|
101
|
+
slug?: string
|
102
|
+
timezone?: string
|
103
|
+
}
|
104
|
+
|
105
|
+
type Category = {
|
106
|
+
id: number
|
107
|
+
slug: string
|
108
|
+
title: string
|
109
|
+
lang: string
|
110
|
+
academy: Academy
|
111
|
+
}
|
112
|
+
|
113
|
+
function getCategoriesByAcademy(
|
114
|
+
categories: Category[],
|
115
|
+
academy: Academy
|
116
|
+
): Category[] {
|
117
|
+
return categories.filter((cat) => cat.academy.id === academy.id)
|
118
|
+
}
|
119
|
+
|
120
|
+
const selectAcademy = async (
|
121
|
+
academies: TAcademy[],
|
122
|
+
bcToken: string
|
123
|
+
): Promise<{ academy: TAcademy | null; category: number }> => {
|
94
124
|
if (academies.length === 0) {
|
95
|
-
return null
|
125
|
+
return { academy: null, category: 0 }
|
96
126
|
}
|
97
127
|
|
98
128
|
if (academies.length === 1) {
|
99
|
-
return academies[0]
|
129
|
+
return { academy: academies[0], category: 0 }
|
100
130
|
}
|
101
131
|
|
102
132
|
// prompts the user to select an academy to upload the assets
|
@@ -110,7 +140,27 @@ const selectAcademy = async (academies: TAcademy[]) => {
|
|
110
140
|
value: academy,
|
111
141
|
})),
|
112
142
|
})
|
113
|
-
|
143
|
+
|
144
|
+
const categories: Category[] = await api.getCategories(bcToken)
|
145
|
+
const categoriesByAcademy = getCategoriesByAcademy(
|
146
|
+
categories,
|
147
|
+
response.academy
|
148
|
+
)
|
149
|
+
|
150
|
+
const categoriesPrompt = await prompts({
|
151
|
+
type: "select",
|
152
|
+
name: "category",
|
153
|
+
message: "Select a category",
|
154
|
+
choices: categoriesByAcademy.map((category) => ({
|
155
|
+
title: category.title,
|
156
|
+
value: category.id,
|
157
|
+
})),
|
158
|
+
})
|
159
|
+
|
160
|
+
return {
|
161
|
+
academy: response.academy,
|
162
|
+
category: categoriesPrompt.category,
|
163
|
+
}
|
114
164
|
}
|
115
165
|
|
116
166
|
class BuildCommand extends SessionCommand {
|
@@ -193,7 +243,10 @@ class BuildCommand extends SessionCommand {
|
|
193
243
|
|
194
244
|
const academies = await api.listUserAcademies(sessionPayload.token)
|
195
245
|
// // console.log(academies, "academies")
|
196
|
-
const academy = await selectAcademy(
|
246
|
+
const { academy, category } = await selectAcademy(
|
247
|
+
academies,
|
248
|
+
sessionPayload.token
|
249
|
+
)
|
197
250
|
|
198
251
|
// Read learn.json to get the slug
|
199
252
|
const learnJsonPath = path.join(process.cwd(), "learn.json")
|
@@ -353,12 +406,15 @@ class BuildCommand extends SessionCommand {
|
|
353
406
|
fs.unlinkSync(zipFilePath)
|
354
407
|
this.removeDirectory(buildDir)
|
355
408
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
409
|
+
if (academy) {
|
410
|
+
await handleAssetCreation(
|
411
|
+
sessionPayload,
|
412
|
+
academy,
|
413
|
+
learnJson,
|
414
|
+
res.data.url,
|
415
|
+
category
|
416
|
+
)
|
417
|
+
}
|
362
418
|
} catch (error) {
|
363
419
|
if (axios.isAxiosError(error)) {
|
364
420
|
if (error.response && error.response.status === 403) {
|
@@ -0,0 +1,192 @@
|
|
1
|
+
import { flags } from "@oclif/command"
|
2
|
+
import * as express from "express"
|
3
|
+
import * as cors from "cors"
|
4
|
+
import * as path from "path"
|
5
|
+
import SessionCommand from "../utils/SessionCommand"
|
6
|
+
import { Storage } from "@google-cloud/storage"
|
7
|
+
import { downloadEditor, decompress } from "../managers/file"
|
8
|
+
import * as fs from "fs"
|
9
|
+
const frontMatter = require("front-matter")
|
10
|
+
|
11
|
+
function getSlugFromPath(path: string) {
|
12
|
+
const parts = path.split("/").filter(Boolean)
|
13
|
+
if (parts.length < 2)
|
14
|
+
return null
|
15
|
+
return parts[parts.length - 2]
|
16
|
+
}
|
17
|
+
|
18
|
+
const bucketStorage = new Storage({
|
19
|
+
keyFilename: path.resolve(__dirname, "../utils/creds.json"),
|
20
|
+
})
|
21
|
+
const bucket = bucketStorage.bucket("learnpack")
|
22
|
+
|
23
|
+
async function listFilesWithPrefix(prefix: string) {
|
24
|
+
const [files] = await bucket.getFiles({ prefix })
|
25
|
+
return files
|
26
|
+
}
|
27
|
+
|
28
|
+
export default class ServeCommand extends SessionCommand {
|
29
|
+
static description = "Runs a small server to build tutorials"
|
30
|
+
|
31
|
+
static flags = {
|
32
|
+
...SessionCommand.flags,
|
33
|
+
port: flags.string({ char: "p", description: "server port" }),
|
34
|
+
host: flags.string({ char: "h", description: "server host" }),
|
35
|
+
debug: flags.boolean({
|
36
|
+
char: "d",
|
37
|
+
description: "debugger mode for more verbage",
|
38
|
+
default: false,
|
39
|
+
}),
|
40
|
+
}
|
41
|
+
|
42
|
+
async init() {
|
43
|
+
const { flags } = this.parse(ServeCommand)
|
44
|
+
console.log("Initializing serve command")
|
45
|
+
}
|
46
|
+
|
47
|
+
async run() {
|
48
|
+
const app = express()
|
49
|
+
const PORT = 3000
|
50
|
+
|
51
|
+
const distPath = path.resolve(__dirname, "../creatorDist")
|
52
|
+
|
53
|
+
// Servir archivos estáticos
|
54
|
+
// app.use(express.static(distPath))
|
55
|
+
app.use(express.json())
|
56
|
+
app.use(cors())
|
57
|
+
|
58
|
+
const appPath = path.resolve(__dirname, "../ui/_app")
|
59
|
+
const tarPath = path.resolve(__dirname, "../ui/app.tar.gz")
|
60
|
+
if (fs.existsSync(appPath)) {
|
61
|
+
fs.rmSync(appPath, { recursive: true })
|
62
|
+
}
|
63
|
+
|
64
|
+
if (fs.existsSync(tarPath)) {
|
65
|
+
fs.rmSync(tarPath)
|
66
|
+
}
|
67
|
+
|
68
|
+
await downloadEditor("5.0.0", `${__dirname}/../ui/app.tar.gz`)
|
69
|
+
|
70
|
+
await decompress(
|
71
|
+
`${__dirname}/../ui/app.tar.gz`,
|
72
|
+
`${__dirname}/../ui/_app/`
|
73
|
+
)
|
74
|
+
|
75
|
+
const localAppPath = path.resolve(__dirname, "../ui/_app")
|
76
|
+
// app.use(express.static(localAppPath))
|
77
|
+
|
78
|
+
app.post("/upload", async (req, res) => {
|
79
|
+
const { content, destination } = req.body
|
80
|
+
// console.log("UPLOAD", content, destination)
|
81
|
+
|
82
|
+
if (!content || !destination) {
|
83
|
+
return res.status(400).send("Missing content or destination")
|
84
|
+
}
|
85
|
+
|
86
|
+
if (!bucket) {
|
87
|
+
return res.status(500).send("Upload failed")
|
88
|
+
}
|
89
|
+
|
90
|
+
const buffer = Buffer.from(content, "utf-8")
|
91
|
+
const file = bucket.file(destination)
|
92
|
+
|
93
|
+
const stream = file.createWriteStream({
|
94
|
+
resumable: false,
|
95
|
+
contentType: "text/plain",
|
96
|
+
})
|
97
|
+
|
98
|
+
stream.on("error", err => {
|
99
|
+
console.error("❌ Error uploading:", err)
|
100
|
+
res.status(500).send("Upload failed")
|
101
|
+
})
|
102
|
+
|
103
|
+
stream.on("finish", () => {
|
104
|
+
console.log(`✅ Uploaded to: ${file.name}`)
|
105
|
+
res.send("File uploaded successfully")
|
106
|
+
})
|
107
|
+
|
108
|
+
stream.end(buffer)
|
109
|
+
})
|
110
|
+
|
111
|
+
app.get("/", async (req, res) => {
|
112
|
+
// The the ui/_app/index.html
|
113
|
+
console.log("GET /")
|
114
|
+
|
115
|
+
const file = path.resolve(__dirname, "../ui/_app/index.html")
|
116
|
+
res.sendFile(file)
|
117
|
+
})
|
118
|
+
|
119
|
+
app.get("/config", async (req, res) => {
|
120
|
+
console.log("GET /config")
|
121
|
+
console.log(req.query.slug, "QUERY")
|
122
|
+
const files = await listFilesWithPrefix("courses/" + req.query.slug)
|
123
|
+
|
124
|
+
const learnJson = files.find(file => file.name.endsWith("learn.json"))
|
125
|
+
|
126
|
+
const learnJsonContent = await learnJson?.download()
|
127
|
+
const learnJsonParsed = JSON.parse(learnJsonContent?.toString() || "{}")
|
128
|
+
const allExercises = files.filter(file =>
|
129
|
+
file.name.includes("exercises/")
|
130
|
+
)
|
131
|
+
const exercises = allExercises.map(exercise => {
|
132
|
+
const slug = getSlugFromPath(exercise.name)
|
133
|
+
return {
|
134
|
+
title: slug,
|
135
|
+
slug,
|
136
|
+
graded: false,
|
137
|
+
}
|
138
|
+
})
|
139
|
+
// console.log(learnJsonParsed, "LEARN JSON PARSED")
|
140
|
+
|
141
|
+
res.send({
|
142
|
+
config: { ...learnJsonParsed },
|
143
|
+
exercises,
|
144
|
+
})
|
145
|
+
})
|
146
|
+
|
147
|
+
app.get("/exercise/:slug/readme", async (req, res) => {
|
148
|
+
console.log("GET /exercise/:slug/readme")
|
149
|
+
const { slug } = req.params
|
150
|
+
const query = req.query
|
151
|
+
console.log(query, "QUERY")
|
152
|
+
const courseSlug = query.slug
|
153
|
+
|
154
|
+
const file = await bucket.file(
|
155
|
+
"courses/" + courseSlug + "/exercises/" + slug + "/README.md"
|
156
|
+
)
|
157
|
+
const content = await file.download()
|
158
|
+
const { attributes, body } = frontMatter(content[0].toString())
|
159
|
+
res.send({
|
160
|
+
attributes,
|
161
|
+
body,
|
162
|
+
})
|
163
|
+
})
|
164
|
+
|
165
|
+
app.get("/assets/:file", (req, res) => {
|
166
|
+
const file = path.join(localAppPath, req.params.file)
|
167
|
+
res.sendFile(file)
|
168
|
+
})
|
169
|
+
// Enviar index.html para todas las rutas
|
170
|
+
app.get("/creator", (req, res) => {
|
171
|
+
res.sendFile(path.join(distPath, "index.html"))
|
172
|
+
})
|
173
|
+
app.get("/creator/syllabus", (req, res) => {
|
174
|
+
res.sendFile(path.join(distPath, "index.html"))
|
175
|
+
})
|
176
|
+
|
177
|
+
app.get("/creator/:file", (req, res) => {
|
178
|
+
console.log("GET /creator/:file", req.params.file)
|
179
|
+
const file = path.join(distPath, req.params.file)
|
180
|
+
res.sendFile(file)
|
181
|
+
})
|
182
|
+
|
183
|
+
app.get("/creator/assets/:file", (req, res) => {
|
184
|
+
const file = path.join(distPath, "assets", req.params.file)
|
185
|
+
res.sendFile(file)
|
186
|
+
})
|
187
|
+
|
188
|
+
app.listen(PORT, () => {
|
189
|
+
console.log(`🚀 Creator UI server running at http://localhost:${PORT}`)
|
190
|
+
})
|
191
|
+
}
|
192
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# React + TypeScript + Vite
|
2
|
+
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
4
|
+
|
5
|
+
Currently, two official plugins are available:
|
6
|
+
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
9
|
+
|
10
|
+
## Expanding the ESLint configuration
|
11
|
+
|
12
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
13
|
+
|
14
|
+
```js
|
15
|
+
export default tseslint.config({
|
16
|
+
extends: [
|
17
|
+
// Remove ...tseslint.configs.recommended and replace with this
|
18
|
+
...tseslint.configs.recommendedTypeChecked,
|
19
|
+
// Alternatively, use this for stricter rules
|
20
|
+
...tseslint.configs.strictTypeChecked,
|
21
|
+
// Optionally, add this for stylistic rules
|
22
|
+
...tseslint.configs.stylisticTypeChecked,
|
23
|
+
],
|
24
|
+
languageOptions: {
|
25
|
+
// other options...
|
26
|
+
parserOptions: {
|
27
|
+
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
28
|
+
tsconfigRootDir: import.meta.dirname,
|
29
|
+
},
|
30
|
+
},
|
31
|
+
})
|
32
|
+
```
|
33
|
+
|
34
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
35
|
+
|
36
|
+
```js
|
37
|
+
// eslint.config.js
|
38
|
+
import reactX from "eslint-plugin-react-x"
|
39
|
+
import reactDom from "eslint-plugin-react-dom"
|
40
|
+
|
41
|
+
export default tseslint.config({
|
42
|
+
plugins: {
|
43
|
+
// Add the react-x and react-dom plugins
|
44
|
+
"react-x": reactX,
|
45
|
+
"react-dom": reactDom,
|
46
|
+
},
|
47
|
+
rules: {
|
48
|
+
// other rules...
|
49
|
+
// Enable its recommended typescript rules
|
50
|
+
...reactX.configs["recommended-typescript"].rules,
|
51
|
+
...reactDom.configs.recommended.rules,
|
52
|
+
},
|
53
|
+
})
|
54
|
+
```
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import js from "@eslint/js"
|
2
|
+
import globals from "globals"
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks"
|
4
|
+
import reactRefresh from "eslint-plugin-react-refresh"
|
5
|
+
import tseslint from "typescript-eslint"
|
6
|
+
|
7
|
+
export default tseslint.config(
|
8
|
+
{ ignores: ["dist"] },
|
9
|
+
{
|
10
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
11
|
+
files: ["**/*.{ts,tsx}"],
|
12
|
+
languageOptions: {
|
13
|
+
ecmaVersion: 2020,
|
14
|
+
globals: globals.browser,
|
15
|
+
},
|
16
|
+
plugins: {
|
17
|
+
"react-hooks": reactHooks,
|
18
|
+
"react-refresh": reactRefresh,
|
19
|
+
},
|
20
|
+
rules: {
|
21
|
+
...reactHooks.configs.recommended.rules,
|
22
|
+
"react-refresh/only-export-components": [
|
23
|
+
"warn",
|
24
|
+
{ allowConstantExport: true },
|
25
|
+
],
|
26
|
+
},
|
27
|
+
}
|
28
|
+
)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8" />
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
|
+
<title>Vite + React + TS</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<div id="root"></div>
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
12
|
+
</body>
|
13
|
+
</html>
|