Skip to content

Module System

ScaffScript’s module system lets you share code between .ss files using export, import, include, and export ... from. These are compile-time only, so they’re fully resolved and stripped before GML output is written.

Marks a declaration as available for other files to consume.

export <var|let|const> <name> = <value>

var compiles to var, const compiles to #macro, and let or no keyword compiles to instance variable.

my-var.ss
export var myVar = 10;
export let myLet = 20;
export const MY_CONST = "hello";
export x = 1;
var myVar = 10; // `var` keyword is preserved
myLet = 20; // `let` keyword is stripped, compiles to instance variable
#macro MY_CONST "hello" // `const` compiles to `#macro`, a global constant
x = 1; // no keyword compiles to instance variable

export function <name>([<args>]) { ... }
my-func.ss
export function print() {
show_debug_message("Hello, World!");
}
export function myFunction(arg1, arg2?) {
show_debug_message(arg1);
}
function print() {
show_debug_message("Hello, World!");
}
function myFunction(arg1, arg2 = undefined) {
show_debug_message(arg1);
}

export const <name> = function([<args>]) { ... }
my-func-expr.ss
export const myFunc = function(arg1) {
show_debug_message(arg1);
}
myFunc = function(arg1) {
show_debug_message(arg1);
}

export const <name> = (<args>) => { ... }

It has the same behavior as a function expression.

my-arrow-func.ss
// multi-line
export const myMethod = (arg1, arg2 = 0) => {
show_debug_message(arg1);
}
// single-line
export const oneLiner = (x?) => show_debug_message(x);
myMethod = function(arg1, arg2 = 0) {
show_debug_message(arg1);
}
oneLiner = function(x = undefined) {
return show_debug_message(x);
}

export enum <name> { ... }
export const enum <name> { ... }

Currently, there’s no difference between those two.

my-enum.ss
export enum MyEnum {
A,
B = 3,
C
}
export const enum MyConstEnum {
X = 20,
Y = 30
}
enum MyEnum {
A, // 0
B, // 3
C // 4
}
enum MyConstEnum {
X = 20,
Y = 30
}

export class <name> {
constructor([<args>])
}

Compiles to a GML struct constructor. Inheritance is not supported yet.

my-class.ss
export class MyClass {
constructor() // constructor is required
field1 = 1;
field2 = 2;
method1() {
show_debug_message("method1");
}
method2 = function() {
show_debug_message("method2");
}
}
export class MyClassConstructor {
constructor(_name, _age = 0) // with parameters and default value
name = _name;
age = _age;
print = function() {
show_debug_message($"Name: {name}");
}
show_age(age?) {
show_debug_message(age);
}
}
function MyClass() constructor {
field1 = 1;
field2 = 2;
method1 = function() {
show_debug_message("method1");
}
method2 = function() {
show_debug_message("method2");
}
}
function MyClassConstructor(_name, _age = 0) constructor {
name = _name;
age = _age;
print = function() {
show_debug_message($"Name: {name}");
}
show_age = function(age = undefined) {
show_debug_message(age);
}
}

Defines a struct shape/template. Members support inline types, default values, and optional flag (?). Supports extends and primitive types (number, string, boolean).

export interface <name> {
<field>[?][: <type>] [= <default>]
}
export interface <name> extends <parent> {
<field>[?][: <type>] [= <default>]
}
my-interface.ss
export interface MyInterface {
name,
age,
address,
is_active?: boolean = true
}
export interface MyExtInterface extends MyInterface {
address: string = "somewhere",
score: number,
phone?
}
my-file.ss
import { MyInterface, MyExtInterface } from "./my-interface"
var user_simple = @use MyInterface {
name: "John",
age: 30,
address: "somewhere"
}
var user_ext = @use MyExtInterface {
name: "Jane",
is_active: false
}
var user_simple = {
name: "John",
age: 30,
address: "somewhere",
is_active: true
};
var user_ext = {
name: "Jane",
address: "somewhere",
is_active: false,
score: 0,
phone: undefined
};

You might wond why the result is different from the original. Here’s why:

  1. interface and type will be compiled as struct literal.
  2. user_ext doesn’t have age set. The age field is stripped in the output, because it’s not set, has no default value, and not an optional field.
  3. user_ext.address is set to "somewhere", because the default value is overridden in MyExtInterface.
  4. user_simple.is_active is true, because default value of is_active is true in MyInterface. It also will be applied to user_ext if you don’t set it.
  5. user_ext.score is 0, because no default value is set for score in MyExtInterface, but it has a data type of number, so it’s compiled to 0.
  6. user_ext.phone is undefined, because it’s optional and no default value is set.

Similar to interfaces, no meaningful difference in runtime behavior. Supports intersection (&) with other type.

export type <name> = {
<field>[?][: <type>] [= <default>]
}
export type <name> = <parent> & {
<field>[?][: <type>] [= <default>]
}
my-type.ss
export type MyType = {
name: string,
age: number,
is_active?: boolean
};
export type MyIntersectedType = MyType & {
address: string = "somewhere",
phone?
};
my-file.ss
import { MyType, MyIntersectedType } from "./my-type"
var user_simple = @use MyType {
name: "John",
age: 30
}
var user_ext = @use MyIntersectedType {
name: "Jane",
is_active: false,
phone: other_var
}
var user_simple = {
name: "John",
age: 30,
is_active: undefined
};
var user_ext = @use MyExtInterface {
name: "Jane",
age: 0,
is_active: false,
address: "somewhere",
phone: other_var
};

Imports the exported modules from another .ss file. The statement itself is stripped from output. Imported names are available via content directives (@content, @valueof, etc.) in the same file.

import { ExportsName } from "path/to/file"
import { Name1, Name2 } from "path/to/file"
import { ExpName: ExpAlias } from "path/to/file" // ExpName -> Alias
import * from "path/to/file" // import all exports from file
my-file.ss
import { ExportsName } from "path/to/file"
import { Name1, Name2 } from "path/to/file2"
import { Original: Alias } from "path/to//deep/file"
import * from "path/to/file-3"
// after import, you can use the imported names
xx = @valueof Alias
show_debug_message($"Name1 = @:Name1, Name2 = @typeof Name2, xx = {xx}");
@content ExportsName

Inlines exported modules or raw .gml files directly at the point of the statement. Shorthand for import + @content for each module.

include { ModuleName } from "path/to/file"
include { Name1, Name2 } from "path/to/file"
include * from "path/to/file" // * = include all exports
export-modules.ss
export let xx = 10;
export const MY_CONST = 20;
export function my_function() {
show_debug_message("Hello, World!");
}
my-file.ss
include { xx } from "./export-modules"
show_debug_message("`xx` has been included");
include { MY_CONST, my_function } from "./export-modules"
xx = 10;
show_debug_message("`xx` has been included");
#macro MY_CONST 20
my_function = function() {
show_debug_message("Hello, World!");
}
  1. include inlines the module at the point of the statement. So, the order of the code is preserved.
  2. Unlike import which only imports the module, include inlines the module at the point of the statement. So, you can not:
    • Use the module using content directives (@content, @valueof, etc.)
    • Alias the module with :
include { "filename.gml" } from "path/to/dir"
include { "file1.gml", "file2" } from "path/to/dir"
raw-gml.gml
show_debug_message("Hello, World!");
dir/other-raw.gml
function other_raw() {
show_debug_message("This one is from other-raw.gml!");
}
my-file.ss
include { "raw-gml.gml" } from "./"
show_debug_message("This is in between");
include { "other-raw" } from "./dir"
show_debug_message("Hello, World!");
show_debug_message("This is in between");
function other_raw() {
show_debug_message("This one is from other-raw.gml!");
}
  1. The raw content of the .gml file is injected at that position. The target can be a file tracked by ScaffScript (from the scan) or any file path on disk.
  2. You must use curly braces and double quotes for including raw GML files. Otherwise, it will be treated as a module, which will cause an error.
  3. The .gml extension in the { <filename> } statement is optional. If not provided, ScaffScript will add it for you.

Re-exports modules from another file, making them available to files that import from the current one.

export { ModuleName } from "path/to/file"
export { ModuleName, OtherModule } from "path/to/file"
export * from "path/to/file"
mod/my-file.ss
export var x = 10;
export interface Shape {
x: number,
y: number
}
export function hello() {
show_debug_message("Hello, World!");
}
exporter.ss
export * from "./mod/my-file" // re-exports all modules from "mod/my-file.ss"

Now you can import x, Shape, and hello from exporter, instead of mod/my-file:

index.ss
import { x, hello } from "./exporter"
@content hello
var my_x = @:x;
function hello() {
show_debug_message("Hello, World!");
}
var my_x = 10;

index.ss acts as a barrel file for its directory. It’s always processed last among files at the same depth, so it can safely re-export everything from sibling files.

my-dir/index.ss
export * from "./some_file"
export * from "./another_file"
other.ss
// import the exported modules from the directory, not the index file directly
// with this, you can import all modules from "./some_file" and "./another_file" at once
import { something } from "./my_dir/index"
import { something } from "./my_dir"

When using extends in an interface, or & in a type, you should extends/intersects one-by-one. For example:

my-interface.ss
export interface MyInterface extends A, B, C {
// ...
}
export type MyType = A & B & C & {
// ...
};