Skip to content

Commit

Permalink
Merge branch 'master' into Video
Browse files Browse the repository at this point in the history
  • Loading branch information
LilyMakesThings authored Aug 25, 2023
2 parents 783e67a + b0f8c20 commit 2b7e599
Show file tree
Hide file tree
Showing 28 changed files with 1,063 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"trailingComma": "es5"
"trailingComma": "es5",
"endOfLine": "auto"
}
20 changes: 8 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Every merged extension is more code that we will be expected to maintain indefin

We're all volunteers who all have lives outside of Scratch extensions. Many have full time jobs or are full time students. We'll get to you as soon as we can, so please be patient.

Every extension is also covered under [our bug bounty](https://github.com/TurboWarp/extensions/security/policy), so mindlessly merging things will have a direct impact on my wallet.

## Writing extensions

Extension source code goes in the [`extensions`](extensions) folder. For example, an extension placed at `extensions/hello-world.js` would be accessible at [http://localhost:8000/hello-world.js](http://localhost:8000/hello-world.js) using our development server.
Expand All @@ -51,7 +53,7 @@ The header comments look like this:
// Original: TestMuffin
```

Remember, this has to be the *very first* thing in the JS file. `Name`, `Description`, and `ID` are required. Make sure that `ID` exactly matches what you return in `getInfo()`. You can have zero or more `By` and `Original`. Put credit links in `<angled brackets>` if you have one. It must point to a Scratch user profile. The parser is pretty loose, but try not to deviate too far from this format.
Remember, this has to be the *very first* thing in the JS file. `Name`, `Description`, and `ID` are required. Make sure that `ID` exactly matches what you return in `getInfo()`. You can have zero or more `By` and `Original`. Put credit links in `<angled brackets>` if you have one. It must point to a Scratch user profile. This metadata is parsed by a script to generate the website and extension library. It tries to be pretty loose, but don't deviate too far. You must use `//`, not `/* */`.

New extensions do not *need* images, but they are highly encouraged. Save the image in the `images` folder with the same folder name and file name (but different file extension) as the extension's source code. For example, if your extension is located in `extensions/TestMuffin/fetch.js`, save the image as `images/TestMuffin/fetch.svg` or `images/TestMuffin/fetch.png`. The homepage generator will detect it automatically. Images are displayed in a 2:1 aspect ratio. SVG (preferred), PNG, or JPG are accepted. PNG or JPG should be 600x300 in resolution. Please add proper attribution to `images/README.md` for *any* resources that were not made by you.

Expand Down Expand Up @@ -123,13 +125,13 @@ If you encounter a TypeScript error, as long as you understand the error, feel f

All pull requests are automatically checked by a combination of custom validation scripts, [ESLint](https://eslint.org/), and [Prettier](https://prettier.io/). Don't worry about passing these checks on the first attempt -- most don't. That's why we have these checks.

Our custom validation scripts do things like making sure you have the correct headers at the start of your extension and that the images are the right size. You can run them locally with:
Our custom validation scripts do things like making sure you have the correct headers at the start of your extension and that the images are the right size. **Your extension must pass validation.** You can run them locally with:

```bash
npm run validate
```

ESLint detects common JavaScript errors such as referencing non-existant variables. You can run it locally with:
ESLint detects common JavaScript errors such as referencing non-existant variables. **Your extension must pass linting.** You can run it locally with:

```bash
npm run lint
Expand All @@ -139,20 +141,14 @@ You are allowed to [disable ESLint warnings and errors](https://eslint.org/docs/

When including third-party code, especially minified code, you may use `/* eslint-disable*/` and `/* eslint-enable */` markers to disable linting for that entire section.

ESLint can automatically fix certain issues. You can run this with:

```bash
npm run fix
```

We use Prettier to ensure consistent code formatting. You can format your code automatically with:
We use Prettier to ensure consistent code formatting. **Your extension does not need to pass format; we will fix it for you if linting and validation pass.** You can format your code automatically with:

```bash
npm run format
```

To just check formatting instead, use:
To just check formatting, use:

```bash
npm run check-format
```
```
122 changes: 109 additions & 13 deletions development/builder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const fs = require("fs");
const AdmZip = require("adm-zip");
const pathUtil = require("path");
const compatibilityAliases = require("./compatibility-aliases");
const parseMetadata = require("./parse-extension-metadata");
Expand Down Expand Up @@ -118,7 +119,7 @@ class ExtensionFile extends BuildFile {
}

class HomepageFile extends BuildFile {
constructor(extensionFiles, extensionImages, mode) {
constructor(extensionFiles, extensionImages, docs, samples, mode) {
super(pathUtil.join(__dirname, "homepage-template.ejs"));

/** @type {Record<string, ExtensionFile>} */
Expand All @@ -127,6 +128,12 @@ class HomepageFile extends BuildFile {
/** @type {Record<string, string>} */
this.extensionImages = extensionImages;

/** @type {Record<string, DocsFile>} */
this.docs = docs;

/** @type {SampleFile[]} */
this.samples = samples;

/** @type {Mode} */
this.mode = mode;

Expand All @@ -144,12 +151,25 @@ class HomepageFile extends BuildFile {
return `${this.host}${extensionSlug}.js`;
}

getDocumentationURL(extensionSlug) {
return `${this.host}${extensionSlug}`;
}

getRunExtensionURL(extensionSlug) {
return `https://turbowarp.org/editor?extension=${this.getFullExtensionURL(
extensionSlug
)}`;
}

/**
* @param {SampleFile} sampleFile
* @returns {string}
*/
getRunSampleURL(sampleFile) {
const path = encodeURIComponent(`samples/${sampleFile.getSlug()}`);
return `https://turbowarp.org/editor?project_url=${this.host}${path}`;
}

read() {
const renderTemplate = require("./render-template");

Expand All @@ -158,10 +178,28 @@ class HomepageFile extends BuildFile {
.slice(0, 5)
.map((i) => i[0]);

/** @type {Map<string, SampleFile[]>} */
const samplesBySlug = new Map();
for (const sample of this.samples) {
for (const url of sample.getExtensionURLs()) {
const slug = new URL(url).pathname.substring(1).replace(".js", "");

if (samplesBySlug.has(slug)) {
samplesBySlug.get(slug).push(sample);
} else {
samplesBySlug.set(slug, [sample]);
}
}
}

const extensionMetadata = Object.fromEntries(
featuredExtensionSlugs.map((id) => [
id,
this.extensionFiles[id].getMetadata(),
featuredExtensionSlugs.map((slug) => [
slug,
{
...this.extensionFiles[slug].getMetadata(),
hasDocumentation: !!this.docs[slug],
samples: samplesBySlug.get(slug) || [],
},
])
);

Expand All @@ -172,6 +210,8 @@ class HomepageFile extends BuildFile {
extensionMetadata,
getFullExtensionURL: this.getFullExtensionURL.bind(this),
getRunExtensionURL: this.getRunExtensionURL.bind(this),
getDocumentationURL: this.getDocumentationURL.bind(this),
getRunSampleURL: this.getRunSampleURL.bind(this),
});
}
}
Expand Down Expand Up @@ -252,6 +292,11 @@ class SVGFile extends ImageFile {
}
}

const IMAGE_FORMATS = new Map();
IMAGE_FORMATS.set(".png", ImageFile);
IMAGE_FORMATS.set(".jpg", ImageFile);
IMAGE_FORMATS.set(".svg", SVGFile);

class SitemapFile extends BuildFile {
constructor(build) {
super(null);
Expand Down Expand Up @@ -284,11 +329,6 @@ class SitemapFile extends BuildFile {
}
}

const IMAGE_FORMATS = new Map();
IMAGE_FORMATS.set(".png", ImageFile);
IMAGE_FORMATS.set(".jpg", ImageFile);
IMAGE_FORMATS.set(".svg", SVGFile);

class DocsFile extends BuildFile {
constructor(absolutePath, extensionSlug) {
super(absolutePath);
Expand All @@ -306,6 +346,44 @@ class DocsFile extends BuildFile {
}
}

class SampleFile extends BuildFile {
getSlug() {
return pathUtil.basename(this.sourcePath);
}

getTitle() {
return this.getSlug().replace(".sb3", "");
}

/** @returns {string[]} list of full URLs */
getExtensionURLs() {
const zip = new AdmZip(this.sourcePath);
const entry = zip.getEntry("project.json");
if (!entry) {
throw new Error("package.json missing");
}
const data = JSON.parse(entry.getData().toString("utf-8"));
return data.extensionURLs ? Object.values(data.extensionURLs) : [];
}

validate() {
const urls = this.getExtensionURLs();

if (urls.length === 0) {
throw new Error("Has no extensions");
}

for (const url of urls) {
if (
!url.startsWith("https://extensions.turbowarp.org/") ||
!url.endsWith(".js")
) {
throw new Error(`Invalid extension URL for sample: ${url}`);
}
}
}
}

class Build {
constructor() {
this.files = {};
Expand Down Expand Up @@ -361,6 +439,7 @@ class Builder {
this.websiteRoot = pathUtil.join(__dirname, "../website");
this.imagesRoot = pathUtil.join(__dirname, "../images");
this.docsRoot = pathUtil.join(__dirname, "../docs");
this.samplesRoot = pathUtil.join(__dirname, "../samples");
}

build() {
Expand Down Expand Up @@ -405,17 +484,31 @@ class Builder {
build.files[`/${filename}`] = new BuildFile(absolutePath);
}

/** @type {Record<string, DocsFile>} */
const docs = {};
for (const [filename, absolutePath] of recursiveReadDirectory(
this.docsRoot
)) {
if (!filename.endsWith(".md")) {
continue;
}
const extensionSlug = filename.split(".")[0];
build.files[`/${extensionSlug}.html`] = new DocsFile(
absolutePath,
extensionSlug
);
const file = new DocsFile(absolutePath, extensionSlug);
docs[extensionSlug] = file;
build.files[`/${extensionSlug}.html`] = file;
}

/** @type {SampleFile[]} */
const samples = [];
for (const [filename, absolutePath] of recursiveReadDirectory(
this.samplesRoot
)) {
if (!filename.endsWith(".sb3")) {
continue;
}
const file = new SampleFile(absolutePath);
build.files[`/samples/${filename}`] = file;
samples.push(file);
}

const scratchblocksPath = pathUtil.join(
Expand All @@ -429,6 +522,8 @@ class Builder {
build.files["/index.html"] = new HomepageFile(
extensionFiles,
extensionImages,
docs,
samples,
this.mode
);
build.files["/sitemap.xml"] = new SitemapFile(build);
Expand Down Expand Up @@ -472,6 +567,7 @@ class Builder {
`${this.imagesRoot}/**/*`,
`${this.websiteRoot}/**/*`,
`${this.docsRoot}/**/*`,
`${this.samplesRoot}/**/*`,
],
{
ignoreInitial: true,
Expand Down
Loading

0 comments on commit 2b7e599

Please sign in to comment.