Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Make sure you have the following software installed:

1. `node.js v24` or higher
2. `git`.
3. `bun`
4. `rust` via `rustup` (this also installs `cargo`)

We require Node.js v24 or higher for compatibility with modern JavaScript features.

Expand All @@ -55,6 +57,22 @@ For some more advanced development operations (such as bulk testing from the com

### Installation Instructions

If you are on macOS, install the required tooling first:

```bash
xcode-select --install
curl -fsSL https://bun.sh/install | bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

After that, close the terminal, open a new one, and verify:

```bash
bun --version
rustc --version
cargo --version
```

1. Clone this repository and run the `install.sh` script. This will install the necessary dependencies and build some bundles that are necessary for the bootstrapping process of `lively.next`. Please note, that this process will take a few minutes.
2. Run the `start.sh` script.
3. Lively will now be running on computer and be accessible at [http://localhost:9011](http://localhost:9011).
Expand Down
62 changes: 37 additions & 25 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ step() { echo " $1"; }
info() { echo " $1"; }
success() { echo " $1 done"; }
warn() { echo " [!] $1"; }
error() { echo " [ERROR] $1"; }

print_bun_install_instructions() {
info " Bun is required for supported installs."
info " Install Bun with:"
info " curl -fsSL https://bun.sh/install | bash"
info " Then restart your terminal and verify with:"
info " bun --version"
}

print_rust_install_instructions() {
info " Rust is required for supported installs."
if [ "$(uname -s)" = "Darwin" ]; then
info " On macOS, install the Apple command line tools first:"
info " xcode-select --install"
fi
info " Install Rust with:"
info " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
info " Then restart your terminal and verify with:"
info " rustc --version"
info " cargo --version"
}

echo ""
echo "lively.next installer"
Expand All @@ -23,33 +45,29 @@ echo "====================="
section "Checking dependencies"
step "node: $(node --version)"

# Check for bun (required for fast package install)
# Check for bun (required for supported package install)
if command -v bun >/dev/null 2>&1; then
export BUN_PATH=$(command -v bun)
step "bun: $(bun --version)"
elif [ -f "$HOME/.bun/bin/bun" ]; then
elif [ -x "$HOME/.bun/bin/bun" ]; then
export BUN_PATH="$HOME/.bun/bin/bun"
export PATH="$HOME/.bun/bin:$PATH"
step "bun: $($BUN_PATH --version)"
else
warn "bun not found — using slow sequential download"
info " Install for ~50x faster installs: curl -fsSL https://bun.sh/install | bash"
error "bun not found"
print_bun_install_instructions
exit 1
fi

# Check for Rust toolchain (needed to build SWC plugin)
PREBUILT_WASM=$lv_next_dir/lively.freezer/swc-plugin/lively_swc_plugin.wasm
if command -v cargo >/dev/null 2>&1 && command -v rustup >/dev/null 2>&1; then
if command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustup >/dev/null 2>&1; then
step "rust: $(rustc --version 2>/dev/null | sed 's/rustc //')"
if ! rustup target list --installed 2>/dev/null | grep -q "^wasm32-wasip1$"; then
info " wasm32-wasip1 target will be added during build"
fi
elif [ -f "$PREBUILT_WASM" ]; then
step "rust: not installed (using pre-built SWC plugin)"
elif [ -n "${CI}" ]; then
step "rust: not installed (CI will use pre-built plugin if available)"
else
warn "Rust not found — required to build SWC plugin"
info " Install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
info " Or ensure pre-built plugin exists at: $PREBUILT_WASM"
error "Rust toolchain not found"
print_rust_install_instructions
exit 1
fi

Expand Down Expand Up @@ -100,19 +118,13 @@ then
fi

section "Building SWC plugin"
if [ -n "${CI}" ] && [ -f "$PREBUILT_WASM" ]; then
step "Using pre-built WASM plugin (CI)"
elif command -v cargo >/dev/null 2>&1; then
if ! rustup target list --installed | grep -q "^wasm32-wasip1$"; then
step "Adding Rust target wasm32-wasip1..."
rustup target add wasm32-wasip1 || exit 1
fi
step "Compiling WASM plugin..."
env CI=true npm --silent --prefix $lv_next_dir/lively.freezer/ run build-swc-plugin || exit 1
step "SWC plugin built"
else
step "Using pre-built WASM plugin"
if ! rustup target list --installed | grep -q "^wasm32-wasip1$"; then
step "Adding Rust target wasm32-wasip1..."
rustup target add wasm32-wasip1 || exit 1
fi
step "Compiling WASM plugin..."
env CI=true npm --silent --prefix $lv_next_dir/lively.freezer/ run build-swc-plugin || exit 1
step "SWC plugin built"

section "Building freezer bundles"
if [ -z "${CI}" ]; then
Expand Down
Binary file added lively.freezer/lively_swc_plugin.wasm
Binary file not shown.
19 changes: 5 additions & 14 deletions lively.freezer/src/bundler-swc.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export class LivelySwcTransform {
resolvedImports = {},
captureImports = true,
sourceMap = true,
filename = 'unknown.js'
filename = 'unknown.js',
moduleHash = null
} = options;

const swcConfig = {
Expand All @@ -79,18 +80,7 @@ export class LivelySwcTransform {
}
};

let classToFunctionConfig = null;
if (classToFunction === undefined) {
classToFunctionConfig = {
classHolder: this.options.captureObj,
functionNode: 'initializeES6ClassForLively',
currentModuleAccessor: 'module.id'
};
} else if (classToFunction === false || classToFunction === null) {
classToFunctionConfig = null;
} else {
classToFunctionConfig = classToFunction;
}
const classToFunctionConfig = classToFunction || null;

const livelyConfig = {
captureObj: this.options.captureObj,
Expand All @@ -108,7 +98,8 @@ export class LivelySwcTransform {
enableDynamicImportTransform: true,
enableSystemjsTransform: false,
enableExportSplit: true,
resolvedImports
resolvedImports,
...(moduleHash != null ? { moduleHash } : {})
};

const classRuntimeModule = resurrection ? 'livelyClassesRuntime.js' : 'lively.classes/runtime.js';
Expand Down
45 changes: 33 additions & 12 deletions lively.freezer/src/bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ export default class LivelyRollup {
};
}

getSwcTransformOptions (modId, source, { instrumentClasses } = {}) {
getSwcTransformOptions (modId, source, { instrumentClasses, moduleHash } = {}) {
if (modId === '@empty.js') return {};
let parsedSource;
try {
Expand Down Expand Up @@ -458,8 +458,13 @@ export default class LivelyRollup {
currentModuleAccessor,
// Keep parity with legacy transform pipeline:
// import capture is only enabled for source-map builds.
// Re-exported imports are handled separately by the SWC transform's
// export { x } from '...' splitting (when capture_imports is false,
// it still splits re-export-from statements for capture).
captureImports: this.sourceMap,
resurrection: this.isResurrectionBuild,
// For resurrection builds, wrap function declarations through __define__
...(this.isResurrectionBuild ? { declarationWrapper: `__varRecorder__["${normalizedId}__define__"]` } : {}),
resolvedImports,
classToFunction: instrumentClasses ? {
classHolder,
Expand All @@ -473,7 +478,8 @@ export default class LivelyRollup {
...arr.range(0, 50).map(i => `__captured${i}__`)
],
sourceMap: this.sourceMap,
filename: modId
filename: modId,
...(moduleHash != null ? { moduleHash } : {})
};
}

Expand Down Expand Up @@ -657,7 +663,24 @@ export default class LivelyRollup {
const exports = ast.query.exports(topLevel.scope, true);
return new Set(exports.map(exp => exp.exported).filter(Boolean));
} catch (err) {
return new Set();
// Fallback: regex-based export detection for code that acorn can't parse
// in strict module mode (e.g., class-to-function transformed output).
const names = new Set();
const exportRe = /export\s*\{([^}]+)\}/g;
const defaultRe = /export\s+default\b/g;
let m;
while ((m = exportRe.exec(source)) !== null) {
m[1].split(',').forEach(spec => {
const parts = spec.trim().split(/\s+as\s+/);
const exported = (parts[1] || parts[0]).trim();
if (exported) names.add(exported);
});
}
if (defaultRe.test(source)) names.add('default');
// Also match `export function/class/const/let/var`
const declRe = /export\s+(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)/g;
while ((m = declRe.exec(source)) !== null) names.add(m[1]);
return names;
}
}

Expand Down Expand Up @@ -799,7 +822,8 @@ export default class LivelyRollup {
// ScopeCapturingTransform at step 7), so System.import() is rewritten
// before System references get captured as __varRecorder__.System.
if (needsLoadInstrumentation) this.hasDynamicImports = true;
const swcOptions = this.getSwcTransformOptions(id, source, { instrumentClasses });
const moduleHash = this.isResurrectionBuild ? string.hashCode(await this.resolver.load(id)) : undefined;
const swcOptions = this.getSwcTransformOptions(id, source, { instrumentClasses, moduleHash });
let { code, map } = await swcTransform.transformAsync(source, swcOptions);
if (this.shouldCompareSwcForModule(id)) {
if (!this._swcComparedModules) this._swcComparedModules = new Set();
Expand All @@ -816,14 +840,11 @@ export default class LivelyRollup {
const missingExports = [...sourceExports].filter(name => !transformedExports.has(name));
const skipMissingExportFallback = id.includes('dompurify@3.3.0/dist/purify.es.mjs');
if (invalidExports.length > 0 || (missingExports.length > 0 && !skipMissingExportFallback)) {
if (this.verbose) {
const diagnostics = [
invalidExports.length > 0 ? `unresolved exports: ${invalidExports.join(', ')}` : '',
missingExports.length > 0 ? `missing exports: ${missingExports.join(', ')}` : ''
].filter(Boolean).join('; ');
console.warn(`\x1b[33m [!] SWC fallback for ${id}; ${diagnostics}\x1b[0m`);
}
return await this.transformWithLegacyPipeline(source, id, needsLoadInstrumentation);
const diagnostics = [
invalidExports.length > 0 ? `unresolved exports: ${invalidExports.join(', ')}` : '',
missingExports.length > 0 ? `missing exports: ${missingExports.join(', ')}` : ''
].filter(Boolean).join('; ');
throw new Error(`SWC transform produced mismatched exports for ${id}: ${diagnostics}`);
}
if (skipMissingExportFallback && missingExports.length > 0 && this.verbose) {
console.warn(`\x1b[33m [!] SWC export parity: ${id}; missing: ${missingExports.join(', ')} (keeping SWC output)\x1b[0m`);
Expand Down
21 changes: 13 additions & 8 deletions lively.freezer/src/util/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,15 +582,17 @@ export function runtimeDefinition () {
exportsOf (moduleId) {
if (!this.registry[moduleId]) return;
const { exports, recorder: rec } = this.registry[moduleId];
// modify in place
if (!rec.__module_exports__) return;
if (!rec.__module_exports__) return rec;
for (let exp in exports) { if (exp === 'default') continue; delete exports[exp]; }
for (let exp of rec.__module_exports__) {
if (exp.startsWith('__rename__')) {
const [local, exported] = exp.replace('__rename__', '').split('->');
exports[exported] = rec[local];
} else if (exp.startsWith('__reexport__')) Object.assign(exports, this.exportsOf(exp.replace('__reexport__', '')));
else if (exp.startsWith('__default__')) exports.default = rec[exp.replace('__default__', '')] ;
else if (exp.startsWith('__default__')) {
const localName = exp.replace('__default__', '');
if (localName in rec) exports.default = rec[localName];
}
else if (exp in rec) exports[exp] = rec[exp];
}
return exports;
Expand All @@ -606,12 +608,15 @@ export function runtimeDefinition () {
let rec = {
[moduleId + '__define__'] (name, type, value, moduleMeta) {
if (Object.isFrozen(this)) return this[name];
// attach meta info
if (value) {
value[Symbol.for('lively-module-meta')] = moduleMeta;
// attach meta info — but don't overwrite if already set
// (initializeES6ClassForLively and ComponentDescriptor.init set it first)
const metaKey = Symbol.for('lively-module-meta');
if (typeof value === 'function') {
if (!value[metaKey]) value[metaKey] = moduleMeta;
try { value.name = name; } catch(e) {}
} else if (value && typeof value === 'object' && !Object.isFrozen(value)) {
if (!value[metaKey]) value[metaKey] = moduleMeta;
}
value.name = name;
// we can also assign the value to the recorder here?
return value;
}
};
Expand Down
2 changes: 2 additions & 0 deletions lively.freezer/swc-plugin/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lively.freezer/swc-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ swc_core = { version = "9.0.0", features = [
"ecma_visit",
"ecma_utils",
"ecma_ast",
"ecma_codegen",
"__parser",
] }
swc_ecma_ast = "=5.0.1"
swc_plugin_macro = "=1.0.0"
Expand Down
Binary file modified lively.freezer/swc-plugin/lively_swc_plugin.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions lively.freezer/swc-plugin/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ pub struct LivelyTransformConfig {
/// Resolved import source -> normalized module id
#[serde(default)]
pub resolved_imports: HashMap<String, String>,

/// Optional module hash code for resurrection builds.
/// When set, emits `__varRecorder__.__module_hash__ = <hash>` after the recorder init.
#[serde(default)]
pub module_hash: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -141,6 +146,7 @@ impl Default for LivelyTransformConfig {
enable_export_split: true,
enable_scope_capture: true,
resolved_imports: HashMap::new(),
module_hash: None,
}
}
}
Expand Down
Loading
Loading