Skip to content

Module System

ScaffScript’s module system lets you share code between .scaff 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.scaff
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.scaff
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.scaff
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.scaff
// 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.scaff
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.scaff
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.scaff
export interface MyInterface {
name,
age,
address,
is_active?: boolean = true
}
export interface MyExtInterface extends MyInterface {
address: string = "somewhere",
score: number,
phone?
}
my-file.scaff
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.scaff
export type MyType = {
name: string,
age: number,
is_active?: boolean
};
export type MyIntersectedType = MyType & {
address: string = "somewhere",
phone?
};
my-file.scaff
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 .scaff 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.scaff
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.scaff
export let xx = 10;
export const MY_CONST = 20;
export function my_function() {
show_debug_message("Hello, World!");
}
my-file.scaff
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.scaff
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.scaff
export var x = 10;
export interface Shape {
x: number,
y: number
}
export function hello() {
show_debug_message("Hello, World!");
}
exporter.scaff
export * from "./mod/my-file" // re-exports all modules from "mod/my-file.scaff"

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

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

index.scaff 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.scaff
export * from "./some_file"
export * from "./another_file"
other.scaff
// 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.scaff
export interface MyInterface extends A, B, C {
// ...
}
export type MyType = A & B & C & {
// ...
};