How to make a package manager (for a custom language!) — ALL PARTS
So — you built a custom language, and you're pretty satisfied with it. Now, where to go next? One apparent option is making a package manager for your language. But if you're like I was, you probably aren't entirely sure just how to go about making one. So, this tutorial is aimed at getting your custom package manager (for your custom lang) ready and running.
Structure
First, let's go over exactly how a package manager works. There are two main "entities" in the package manager:
- The CDN, or Content Delivery Network, which stores all the actual data for the packages
- The command-line tool, which queries the CDN to find the package you request
So, if I say some-manager -install cool_package
, the following steps (or something similar) will occur:
- The command-line tool will make a request to the CDN, asking "where can I find the latest version of this package?"
- The CDN will respond with the location of the package data it hosts.
- The command-line tool will then fetch the data.
- The command-line tool will install the data in some directory where your language will look for packages, say
./pkg_data
.
Got it? Great! Let's start coding. For this part of the tutorial, we'll be implementing the CDN (as the title advertises). Next time, we'll implement the command line tool.
Implementation of the CDN
Let's go over our task for this section:
- We need to make something which can store and find files, sorted by their various packages.
- We need to make something which will respond to HTTP requests. There are three possible "types" of requests:
/package/<packagename>
should all the versions of the package./package/<packagename>/<version>
should give an index of all the files in that package./package/<packagename>/<version>/<file>
should give each specific file.
Whew, that's a lot! So, let's tackle this step-by-step. (You can follow along at https://repl.it/@fuzzyastrocat/pkg-cdn if you like.)
I'm going to use nodeJS for this, since it's easy to manage files and with express
serving requests is easy as well. However, you can use the language of your choosing, just note that you might have to translate some things.
First, let's make a folder called packagedata
. This is where all the packages will be stored. This tutorial will not cover how those packages get there (i.e, getting published like npm does), for now we'll just put a static package there as an example. Speaking of which, let's make a folder inside packagedata
named example
. Inside this folder will be all the versions of this package called example
. So finally, inside./packagedata/example
let's add a folder named "1.0.0".
Your filetree should now be:
folder packagedata - folder example - folder 1.0.0
Inside "1.0.0", we'll put a file. Let's call it main.lang
(.lang
can be replaced with your custom lang's extension, I'm just using it as a placeholder.) This will be the file served by our CDN when the command-line tool requests /example/1.0.0/main.lang
.
That was a lot of setup, if you're lost just check the repl for this section and update yours to match it. Okay, now the fun part — code.
In index.js
(which I'm assuming repl has created for you), let's set up our basic web server:
const express = require('express'), app = express(); app.get('/', (req, res) => { res.send('Yay it works!') }); app.listen(3000, () => { console.log('CDN running'); });
For those familiar with express
, this should look familiar. If you're not, all you need to know for this tutorial is that app.get('/some/path/', (req, res) => {...});
is how you handle requests to your server. To make sure everything is working, run the repl and go to its address (since mine is named pkg-cdn
, I go to https://pkg-cdn.fuzzyastrocat.repl.co
). You should see "Yay, it works!".
Okay, now we can start implementing functionality. Let's start by getting all of our packages and creating a list of them. After app = express();
, write:
const fs = require('fs'), packageList = fs.readdirSync("./packagedata/");
Here we're just getting all the files inside "packagedata" so that we know what packages we have. Great! Now we can properly know which packages exist. After app.get('/', (req, res) => {...});
, let's write the following:
app.get('/package/:packagename', (req, res) => { if(packageList.includes(req.params.packagename)) res.send("This package exists"); else res.send("This package does not exist."); });
This will serve requests like /package/my_package
and grab the name of the package we're trying to find (in the example, my_package
). Then, we see if it's in our list of packages.
Let's look at what we have so far:
const express = require('express'), app = express(); const fs = require('fs'), packageList = fs.readdirSync("./packagedata/"); app.get('/', (req, res) => { res.send('Yay it works!') }); app.get('/package/:packagename', (req, res) => { if(packageList.includes(req.params.packagename)) res.send("This package exists"); else res.send("This package does not exist."); }); app.listen(3000, () => { console.log('CDN running'); });
If we try to go to https://your-url.repl.co/package/test
, it should correctly respond with This package does not exist.
. But if we go to https://your-url.repl.co/package/example
, it should say This package exists
. If your code does this, then we can move on to the next step — giving version data.
Under where we define packageList
, let's put the following:
const packages = {}; for(let packagename of packageList){ packages[packagename] = {}; for(let version of fs.readdirSync("./packagedata/" + packagename + "/")){ packages[packagename][version] = []; for(let file of fs.readdirSync("./packagedata/" + packagename + "/" + version + "/")){ packages[packagename][version].push(file); } } }
This essentially converting the filetree to a JSON structure — after this operation is performed, packages
will be the following:
{ "example": { "1.0.0": ["main.lang"] } }
Now, we can change our request code:
app.get('/package/:packagename', (req, res) => { const name = req.params.packagename; if(packageList.includes(name)) res.send(packages[name]); else res.status(404).send("This package does not exist."); });
Here's the completed code for this section:
const express = require('express'), app = express(); const fs = require('fs'), packageList = fs.readdirSync("./packagedata/"); const packages = {}; for(let packagename of packageList){ packages[packagename] = {}; for(let version of fs.readdirSync("./packagedata/" + packagename + "/")){ packages[packagename][version] = []; for(let file of fs.readdirSync("./packagedata/" + packagename + "/" + version + "/")){ packages[packagename][version].push(file); } } } app.get('/', (req, res) => { res.send('Yay it works!') }); app.get('/package/:packagename', (req, res) => { const name = req.params.packagename; if(packageList.includes(name)) res.send(packages[name]); else res.status(404).send("This package does not exist."); }); app.listen(3000, () => { console.log('CDN running'); });
Now, when we go to https://your-url.repl.co/packages/test
we should get the same error, but if we go to https://your-url.repl.co/packages/example
we should get {"1.0.0":["main.lang"]}
. Hooray, now we can finish the basic serving mechanism.
Right above app.listen(3000, () => {...
, we add the following:
app.get('/package/:packagename/:version', (req, res) => { const name = req.params.packagename, version = req.params.version; if(packageList.includes(name) && packages[name][version]) res.send(packages[name][version]); else res.status(404).send("This package or version does not exist."); }); app.get('/package/:packagename/:version/:file', (req, res) => { const name = req.params.packagename, version = req.params.version, file = req.params.file; if(packageList.includes(name) && packages[name][version] && packages[name][version].includes(file)) res.send(fs.readFileSync("./packagedata/" + name + "/" + version + "/" + file, "utf8")); else res.status(404).send("This package or version or file does not exist."); });
I won't explain this in detail — it's just an extension of the /package/:packagename
request mechanism. To test, first add something in main.lang
(some text, any text) and then run the repl. Then try going to https://your-url.repl.co/package/example/1.0.0/main.lang
: that text should be shown! If you change any part of the url though, it should say something like "package or file does not exist" depending on what you change.
If that works, hooray! We've just created our CDN. (Complete code can be found in the example repl, in the file index.js
.)
Conclusion
We've now implemented a basic CDN. This is just a starting point — right now it's very basic, and later we may adapt and improve it.
Stay tuned for part 2, where we'll implement the command-line tool and actually make our packager!
Part 2
I'm merging these tutorials for easy reading. So, here's Part 2.
Recap
Let's quickly go over what we've done so far.
- We've built a CDN, which stores package data and will deliver it to our tool.
- Our CDN responds to
GET
requests on the url/package/<packagename>/<version>/<file>
. Any invalid fields will result in a 404 error and a message such as "This package does not exist".
So, our task at hand: build a command-line tool which gets the package data from the CDN and places it in a nice location. For this tutorial we'll use ./pkg_data/
.
Let's get started! (The example repl can be found at https://repl.it/@fuzzyastrocat/cmd-line. As with before, feel free to follow along if you like.)
Language Choice
For this section, I'm going to use Go. I'm using Go because of two reasons:
- Go works nicely for HTTP things
- Go is compiled
This second one is especially important here — we need our tool to be compiled since we want to be able to create an executable from it. So, as with last time, it's okay if you want to use a different language, but consider carefully based on the two criteria above.
Implementation of the Command-Line Tool
First things first, we need to set up our repl.it workspace. Make a new Go project. Inside our new Go project, create a file called .replit
. This is a magical special file that repl.it looks at when you click the run button. In it, put the following:
language = "go" run = "go build -o packager -ldflags \"-s -w\" main.go"
This tells repl.it that our repl's main language is Go and that when we click the run button it should execute go build -o packager -ldflags \"-s -w\" main.go
. (The ldflags
just tell the go compiler not to generate version or debug info embedded in the executable. We don't really need to worry about that info since this will become a standalone executable.) -o packager
tells Go that we want to output our new executable to ./packager
. DO NOT remove this or use -o main
: repl will hide an executable called ./main
by default, so you won't realize that the program has compiled!
The .replit
file overrides repl's default behavior, which is to run the go file. Instead, we simply build
it, meaning that an executable (./packager
) is generated. We can then run this executable by typing ./packager
in the shell (on the right). When you are done with your packager, this will be the actual tool — that way, you can run it without any need for the go
compiler.
Okay, now that we've set up our project let's start implementing our interface. The first thing we need to do is check what package the user wants. When you run the executable like ./packager install some_package
, ./packager
, install
and some_package
will get given to Go in the form of an array called os.Args
. To use it, we need to import os
. Let's make a quick program that just echoes the arguments we give it:
package main import ( "fmt" "os" ) func main() { for _, name := range os.Args[1:] { fmt.Println(name) } }
Now, click the Run button of your repl. Wait for it to finish running — you should see a file named "packager" get generated. Once it's done building, type ./packager these are some arguments
into the shell on the left — the program should print those arguments back to you.
"Wait, why do we ignore the first argument?" Remember that the first argument will always be ./packager
, and we don't care about the name of the program.
Okay, let's do something more useful with those arguments. We'll expect arguments of the following form:
./packager install [packagename]
We'll start by making the only option install
— we include this only for later extensibility. Let's simply start by printing out the available versions of a package:
package main import ( "fmt" "os" "net/http" "io/ioutil" ) func main() { if os.Args[1] != "install" { fmt.Println("Invalid option " + os.Args[1] + "!") os.Exit(1) } packagename := os.Args[2] resp, _ := http.Get("https://your-cdn-url.repl.co/package/" + packagename) if resp.StatusCode == 404 { fmt.Println("That package does not exist!") os.Exit(1) } body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)); }
Obviously, replace "your-cdn-url.repl.co" with your actual cdn url from the last tutorial.
Run the repl, and try ./packager install foo
. It should respond with "That package does not exist!". Try again with ./packager install example
(assuming that you followed the previous tutorial), and it should print out ["1.0.0"]
.
Okay, now we need to figure out how to parse this array. We'll make use of Go's encoding/json
package, since it parses JSON arrays nicely:
package main import ( "fmt" "os" "net/http" "io/ioutil" "encoding/json" ) func main() { if os.Args[1] != "install" { fmt.Println("Invalid option " + os.Args[1] + "!") os.Exit(1) } packagename := os.Args[2] resp, _ := http.Get("https://pkg-cdn.fuzzyastrocat.repl.co/package/" + packagename) if resp.StatusCode == 404 { fmt.Println("That package does not exist!") os.Exit(1) } versions, _ := ioutil.ReadAll(resp.Body) var versions_arr []string; json.Unmarshal(versions, &versions_arr) fmt.Printf("Versions: %v\n", versions_arr) }
Note that versions
is a []byte
, which is what json.Unmarshal
takes as an argument, so we don't do any string()
conversion. If all goes correctly, ./packager install example
should now print out "Versions: [1.0.0]".
Now we need to figure out how to find the latest version. We'll assume that all versions all well-formed sequences of 3 numbers separated by a .
(things like 1.2.3
and 2.10.0
are fine but 0..0
and 0.5
are not), which makes writing a compare function easier:
func later_version(a string, b string) string { sa := strings.Split(a, ".") sb := strings.Split(b, ".") for i := range sa { ia, _ := strconv.Atoi(sa[i]) ib, _ := strconv.Atoi(sb[i]) if(ia > ib){ return a } if(ib > ia){ return b } } return a }
Now, we just need to iterate through the array of versions to find the latest (we assume there is at least one version):
largest := versions_arr[0] for i := range versions_arr { largest = later_version(largest, versions_arr[i]) }
So now we can tie it all together to find the files in the package:
package main import ( "fmt" "os" "net/http" "io/ioutil" "encoding/json" "strings" "strconv" ) func later_version(a string, b string) string { sa := strings.Split(a, ".") sb := strings.Split(b, ".") for i := range sa { ia, _ := strconv.Atoi(sa[i]) ib, _ := strconv.Atoi(sb[i]) if(ia > ib){ return a } if(ib > ia){ return b } } return a } func main() { if os.Args[1] != "install" { fmt.Println("Invalid option " + os.Args[1] + "!") os.Exit(1) } packagename := os.Args[2] resp, _ := http.Get("https://your-cdn-url.repl.co/package/" + packagename) if resp.StatusCode == 404 { fmt.Println("That package does not exist!") os.Exit(1) } versions, _ := ioutil.ReadAll(resp.Body) var versions_arr []string; json.Unmarshal(versions, &versions_arr) latest := versions_arr[0] for i := range versions_arr { latest = later_version(latest, versions_arr[i]) } resp, _ = http.Get("https://your-cdn-url.repl.co/package/" + packagename + "/" + latest) files, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(files)) }
Once again, replace your-cdn-url.repl.co
with your actual cdn url. If running ./packager install example
prints out ["main.lang"]
(or whatever your example filename was), great! We can now move on to actually (finally!) fetching the files.
First, we need to parse the array. Then, for each file in the array, we need to fetch (http GET) the contents of the file, which we can then save in a local directory. First though, we'll just print the contents of each file. Replacing fmt.Println(string(files))
, here is the relevant code to complete this task:
var files_arr []string; json.Unmarshal(files, &files_arr) for _, file := range files_arr { resp, _ = http.Get("https://your-cdn-url.repl.co/package/" + packagename + "/" + latest + "/" + file) contents, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(contents)) }
(Replace your-cdn-url.repl.co
as needed.) I won't explain this in detail because there's really no new code here. We're just doing what we've done before, except extended slightly.
So... try it out! Last tutorial, I put Test file!
as the contents of main.lang
. Therefore, I should expect Test file!
to be printed to the console (which it was). If your program does this correctly, then we can move on to the final section: doing something useful with this file data.
First of all, we need to create a folder called ./pkg_data
if it doesn't exist. This is easy with os.Mkdir
:
os.Mkdir("./pkg_data", 0755)
(This code goes at the top of main()
, you'll see it in the completed code linked below.) 0755
is a permissions number, it means that you'll be able to do anything with the folder and other users can only read it and enter it (but not modify it).
After we get the packagename
from os.Args
, we need to create a folder for the package. We can do that in the same way as above, with os.Mkdir
:
os.Mkdir("./pkg_data/" + packagename, 0755)
Now we can finally write the files into our local package location. We can write to and make a file with os.Create
:
file, _ := os.Create("./moduledata/" + packagename + "/" + file) file.Write(contents) defer file.Close()
(This replaces fmt.Println(string(contents))
.) Okay, the big moment has come: try building and running ./packager install example
. If all goes well, you should have a file main.lang
inside ./moduledata/example/
, with the contents you put in your CDN!
Completed code can be found here. Obviously, replace all occurrences of pkg-cdn.fuzzyastrocat.repl.co
with your CDN url.
Conclusion
Wow, that was a lot of work for an utterly trivial result! But now, we do have a minimal package manager which can fetch and "clone" packages from our CDN. Play around with the CDN — try adding more files to the example
package and use your command line tool to fetch them. Just remember to re-run your CDN (not the command line tool!) every time you add or change hosted package files. (Note that right now our install
option both installs and/or updates, we'll separate out that duty later.)
The nice thing about using a compiled language is that you can take the ./packager
executable, put it anywhere (within the same type of system, obviously a Windows-generated executable won't work on a Mac), rename it, do whatever (without modifying the contents), and it will still work the same way. It's a standalone executable!
Next time, we'll implement more functionality for our command line (like update
and remove
) and make it a truly usable package manager!
Part 3
And now, for part 3:
The task
For this tutorial, we'll be wrapping up the loose ends. We'll do the following:
- Finish our command line tool: give it
update
andremove
functionality - Add to our CDN: Allow users to publish packages to it
- Add accounts
Setup
The example repls for this section can be found here and here if you'd like to follow along. (They're new repls since we'll be changing both aspects of our package manager.) There's not much setup this time since we're just expanding our existing work.
In contrast to the previous parts, we'll be adding lots of stuff in different places. So, I would highly recommend cloning both example repls, and then just following-along with this tutorial. (Use this like a code-guide of how I got to the finished project)
Adding functionality to the command-line tool
First, let's allow our users to do something else than install
:
if os.Args[1] != "install" && os.Args[1] != "update" && os.Args[1] != "remove" { fmt.Println("Invalid option " + os.Args[1] + "!") os.Exit(1) }
(This replaces the original install check.) Now we need to handle each of these options. We can actually handle all three cases "at the same time". Under packagename := os.Args[2]
we add the following:
if _, err := os.Stat("./pkg_data/" + packagename); os.IsNotExist(err) { if os.Args[1] == "update" || os.Args[1] == "remove" { fmt.Println("Package " + packagename + " is not installed!") os.Exit(1) } } else if os.Args[1] == "install" { fmt.Println("Package " + packagename + " is already installed!") os.Exit(1) }
You can probably reason out what this does: if the package's folder doesn't exist and we're trying to update or remove it, we say that it isn't installed and exit. If it does, and we're trying to install it, we say that it's already installed and exit. There's probably a more elegant way to do this, but it works.
Note: I'm using nested if's for a reason! If the inner if
was combined with the outer if
, the else if
clause would behave differently.
Now let's handle remove
's functionality (recall that our install
behaved like an update or install, so we've already taken care of the functionality for those two). Under the code we just added, add the following:
if os.Args[1] == "remove" { os.RemoveAll("./pkg_data/" + packagename) os.Exit(0) }
Easy! We've now completed task 1: test out ./packager update ...
, ./packager install ...
, and ./packager remove ...
to make sure the changes work. It should give you an error if you try to do something invalid, ie update a package which isn't installed or install a package which is already installed.
Adding to the CDN: Publishing functionality
Okay, now head on over to your CDN. First, we define a function updatePackages
so that we can update the package list once a new package is published. If you didn't sync with the example repl, I'd suggest doing that now. updatePackages
replaces some code around it, which could be confusing to update and lead to wrong code.
We need to respond to a new kind of request — an HTTP POST
request. POST
requests have the url and a "body" which contains the important data. We handle our request like so:
app.post('/publish/:packagename', (req, res) => { const name = req.params.packagename, data = req.body; fs.mkdirSync('./packagedata/' + name, { recursive: true }); // Recursive mimics -p try { fs.mkdirSync('./packagedata/' + name + '/' + data.version); } catch (e){ res.send("VERSION_EXISTS"); return } for(let file in data){ if(file === "version") continue; fs.writeFileSync("./packagedata/" + name + '/' + data.version + "/" + file.slice(1), data[file], "utf8"); } updatePackages(); res.send(); // Acknowlege the request was OK with an empty 200 response })
This is placed after app.get('/', (req, res) => { ... });
. The code here responds to POST
requests on /publish/<some_package_name>
. It looks at the body, which should be JSON, makes the necessary directory for the package (./packagedata/<some_package_name>
) and makes a folder with the new version (./packagedata/<some_package_name>/{data.version}
). Finally, we loop through all the keys in the JSON object (which are not the version
key) and make a new file for each with the contents of the file equal to the value of that key. We use file.slice(1)
since each filename will be prefixed by a _
, to differentiate a file named "version" from the actual version number.
To parse the body as JSON, we need to add a new dependency to our project. Below app = express()
, we write the following:
const bodyParser = require('body-parser'); app.use(bodyParser.json());
This does what you probably think it does — allows the app to parse JSON in a POST
request body.
Command-line updates
Now let's make updates on the command-line side so that we can actually publish things. We add || os.Args[1] != "publish"
to our initial if
-statment check, add "bytes"
to our imports, and add the following after packagename := os.Args[2]
(check the completed code if you are lost):
if os.Args[1] == "publish" { jsonmap := map[string]string { "version": os.Args[3], } files, _ := ioutil.ReadDir("./" + packagename) for _, f := range files { dat, _ := ioutil.ReadFile("./" + packagename + "/" + f.Name()) jsonmap["_" + f.Name()] = string(dat) } requestBody, _ := json.Marshal(jsonmap) res, _ := http.Post("https://your-cdn-url.repl.co/publish/" + packagename, "application/json", bytes.NewBuffer(requestBody)) respcode, _ := ioutil.ReadAll(res.Body); respstr := string(respcode) if respstr == "VERSION_EXISTS" { fmt.Println("That package version already exists!") os.Exit(1) } fmt.Println("Package published.") defer res.Body.Close() os.Exit(0) }
So, if you use ./packager publish test 1.0.0
, it will look in the directory ./test
, take all the files from there and publish them to the CDN (with version 1.0.0). Let's make a directory, ./test
, and put a few files inside. Then try ./packager publish test 1.0.0
! It should say "Package published successfully", and when you go to your-cdn.repl.co/package/test
, it should give you ["1.0.0"]
instead of "This package does not exist". Great!
Finishing up
There's one problem with our system so far: anyone can update a package, regardless of who first made it! Let's fix that.
Inside ./test
, we add a file .user
. Inside that file we put me
or whatever name you like. This will be your "user" that gets published with the package to the CDN. Now, on the CDN, we add logic. We replace our original app.post
handler with the following:
app.post('/publish/:packagename', (req, res) => { const name = req.params.packagename, data = req.body; try { fs.mkdirSync('./packagedata/' + name); } catch (e){ if (data[".user"] !== fs.readFileSync('./packagedata/' + name + '/.user', 'utf8')){ res.send("PACKAGE_EXISTS") return } } try { fs.mkdirSync('./packagedata/' + name + '/' + data.version); } catch (e){ res.send("VERSION_EXISTS"); return } fs.writeFileSync("./packagedata/" + name + "/.user", data[".user"], "utf8") for(let file in data){ if(file === "version" || file === ".user") continue; fs.writeFileSync("./packagedata/" + name + '/' + data.version + "/" + file.slice(1), data[file], "utf8"); } updatePackages(); res.send(); // Acknowlege the request was OK with an empty 200 response })
Now, we need to handle PACKAGE_EXISTS from the command line. After if respstr == "VERSION_EXISTS" { ... }
, we put the following:
if respstr == "PACKAGE_EXISTS" { fmt.Println("That package already exists!") os.Exit(1) }
Great! If you still haven't, you probably want to sync-up with the example repl since there's been so many changes (some of which might not have been covered here).
Now, try this: publish your package "test" again (with the new .user
file). Make sure that works without saying "That package already exists!". If it says that the version exists, just change the version (you've already published that version). Once you've successfully published your package with the .user
, try changing the .user
file and the version and uploading again. This should say "That package already exists!", since you are "a different user".
As always, check the example repls for complete code.
Next steps
That's the end of this tutorial series!
But don't stop here! There's a lot you could do to improve it. The first thing would be to add passwords to your user system. This is a fairly easy addition that will add more (but not complete) security to your packager. Next, I'd add friendlier errors. For instance, try ./packager
with no arguments. (Oh no, a Go panic!) This is another easy addition that will make your packager much more refined. Then, there are a few more complex additions you could try:
- Add a login system through your packager tool itself. Make a command,
./packager login [username] [password]
that would store your user data somewhere. Then, rather than having to create a.user
file every time you want to make a package, you could just run./packager init
and it would read from the stored user data and put that in a new.user
file. - Make sure all versions are correct. To do this, you could take
npm
's route: instead of./packager publish test 1.5.2
, you just do./packager publish test [major or minor or patch]
and it takes care of generating the version for you. - Allow folders in a package. Right now we only take the files in the current directory, add a function to recursively traverse the directories too and send that through the JSON.
- Add an
unpublish
function. Remove the directory, then callupdatePackages()
.
There's so much more to add — the only limit is your imagination!
Conclusion
I hope you've enjoyed these tutorials (I sure have), and I hope they've helped. If you have any questions, please ask!
accept my team invitation
@TravisRaney Why should I? I've been given no information about the team. If I want to join the team I will — but since I know nothing about it, I can't make a decision.
@fuzzyastrocat look I got you on my team deal and its saying pending okay
@TravisRaney That statement is incorrect. You have asked me to be on your team — it is up to me to decide whether I want to accept. I'd like to know more about the team before I randomly and blindly accept a team invite.
@fuzzyastrocat well get this that $100,000 grand prize or something about the language jams so that is the reason why I am creating a team
@TravisRaney That was two months ago. The jam has been over for quite some time.
@fuzzyastrocat great :(
This is a really nice tutorial. Maybe combine 1 and 2? But this is super long so I wouldn't consider this cycle squeezing.
@HahaYes Thanks! When I write part 2, yes I might combine them. The reason I was thinking of separating them is because part 2 will have a lot of code, so it will be pretty long... but I might combine them.
@fuzzyastrocat yep. I don't know whether to learn Javascript or C#. Completely random but whatever. I might consider game dev soon
@HahaYes Learning js will be easier. Writing JS will also probably be easier. I personally find that I can produce working code quickest in JS, with Python coming in a close second place. There are some good libraries for game dev in JS — I personally like p5.js
for 2D and three.js
for 3D. However, if you're thinking of using a game engine (unity, etc) then you'll definitely want to learn C#.
@fuzzyastrocat yeah... maybe I'll just learn both XD
@fuzzyastrocat I have some knowledge from C++, so learning another lang should be easier?
@HahaYes Learning new languages is always a good thing :D
@HahaYes C# yes, JS maybe. What other langs do you know?
@fuzzyastrocat uh I mean... C++ has always been my goto lang.
@HahaYes goto
lang... heh heh
Learning C# will feel natural, since it's in the family of C-style languages. The syntax of JS will be easy, since it's very C-like, however the semantics might be a little hard at first since it's more high-level. But it shouldn't be too hard.
@fuzzyastrocat I personally struggled with learning with Python haha but Js seems a bit more friendly to me. C# is pretty nice. Its basically rip-off java. Heh goto get it (C)
@HahaYes Yeah, Python is interesting because it's a unique style (the oop paradigm in particular). JS is definitely closer to the C languages than Python. (you has partaken in the funny)
@fuzzyastrocat , damn i have a thousand line+ tutorial coming up. lol. I already have surpassed 1100 lines(1000 are by me)
@EpicGamer007 In my opinion I'd split that, I feel like the audience will lose attention if it gets really long.
@fuzzyastrocat , really? That might be a good idea... IDK
@EpicGamer007 I finally finished my tutorial, I'm glad it's done lol... and mine isn't even as long as yours I don't think. So yeah I think you'll probably want to split yours up
@fuzzyastrocat oh no don't do that mods don't like that
@HahaYes Replied to wrong person, sorry about that. Clarified my comment too.
@fuzzyastrocat ehhh its not a mass ping. But I'm neutral on this I guess
@HahaYes Wait, what are you responding to?
@fuzzyastrocat squid's
@HahaYes oh ok, makes more sense now
@HahaYes c# has a better standard library and its beats java most of the time
@HahaYes it also has great documentation
@DungeonMaster00 unlike java (not js)
@HahaYes so learn c#
@DungeonMaster00 there are also many other great things i have not mentioned
Wow! Me and firefish were actually working on one (and when I say "we" I mean he did all the work cause I clueless) and this cleared up a lot of things (even though we're doing it in Go)! Can't wait for part 2!
@DynamicSquid Nice timing! (Great to know you liked it! Part 2 will be coming shortly!)
(Also, I feel like we have some psychic connection or something... I was thinking of using Go for the command-line tool since it's compiled and works well for http stuff... wow)
@DynamicSquid yeah, saw what you guys are doing in github... maybe I can join?
@DynamicSquid Part 2 is out!
@DynamicSquid ...and now part 3 is out
@fuzzyastrocat oh no don't do that mods don't like that
@HahaYes Wait, why? Squid had expressed interest in part 2 so I thought I'd keep them updated... should I not?
I am so jealous of node devs, it is so good for so many cool things. I wanna learn so bad
@EpicGamer007 I'd recommend it! Node is perfect for something like this, it works very nicely for web servers.
@fuzzyastrocat , ive tried so many tutorials, but they all taught me nothing. I have learned a bit more cuz of zave who was really helpful. But I feel like I only scratched the surface. I wanna learn moreee
@EpicGamer007 Unfortunately, I think the best way to learn node is just to try doing things with it, and then repeatedly search stackoverflow until you find what you need to solve the problems :D That's basically how I learned it, just a lot of experience and a lot of googling.
@fuzzyastrocat , hmmmmm, i see. Thanks for the info!
@EpicGamer007 No problem! If you have any node-related questions feel free to ask me.
@fuzzyastrocat , thanks a lot!
Lol imagine using node I have a really cooler idea for a package manager but you'll have to wait until it is released to find my idea out @fuzzyastrocat
Cool tutorials btw
@Codemonkey51 , "really cooler" - Best grammarer
Can't wait for it!
I edited the message while typing lol, get ready for the package manager for cookey lang @EpicGamer007
@Codemonkey51 , ooooh that seems cool, a jam lang
Lol ye I better get back to coding it @EpicGamer007
@Codemonkey51 , good luck! Oh yea , u made the replitdb client for python right, i just wanna say thank you cause your thing really inspired me ;) I made one and it has already been used so big yay :D
Oo This sounds interesting! XP
@Highwayman Great! I'll be writing part 2 fairly soon, so hope you enjoy!
(And thanks for the upvote, this tutorial isn't way at the bottom any more :D)
Awesome! And yw! 😜@fuzzyastrocat
I'm getting an error in my repl. It says
Cannot find module 'express'
@ELDER054 Replit might have changed their auto-install capability, try installing the
express
package withnpm install express
(in the shell) first and see if that works.Ok, express works, but now i'm getting
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x65d5e0]
goroutine 1 [running]:
main.main()
/home/runner/cmd-line/main.go:39 +0x220
@fuzzyastrocat
@ELDER054 I don't see this error when I run your repl, did you resolve it?
It's in the Go repl. If I type
./packager install example
, it gives me that error @fuzzyastrocat@ELDER054 Well given the line number (39) I think I've found your problem. Try going to
https://pkg-cdn.ellder054.repl.co/package/
and see what happens. Looks like you wrote one too many l's in your username.Oh, Thank you so much! @fuzzyastrocat