Skip to content

WIP: Using rollup for theme JS compilation #33103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2159ca4
wip rollup theme experiment
davidtaylorhq Jan 2, 2025
601f6b5
wip
davidtaylorhq May 8, 2025
37859e5
bump
davidtaylorhq May 8, 2025
79d9755
crypto
davidtaylorhq May 8, 2025
ef410fc
bump
davidtaylorhq May 8, 2025
63b88d7
gjs
davidtaylorhq May 8, 2025
de2a11d
tidy
davidtaylorhq May 9, 2025
ddb1ea1
hax
davidtaylorhq May 9, 2025
50efce5
wip
CvX May 13, 2025
717e7b7
lockfile
CvX May 13, 2025
5b8b8f2
single-pass version
davidtaylorhq May 13, 2025
60ef424
bump
davidtaylorhq May 15, 2025
9ada91d
translations
davidtaylorhq May 15, 2025
d906703
todos
davidtaylorhq May 15, 2025
b4456b1
todo2
davidtaylorhq May 15, 2025
fca15c8
compatModules
davidtaylorhq May 20, 2025
9e77984
hbs
davidtaylorhq May 20, 2025
93ae79a
extensionsearch-wip
davidtaylorhq May 20, 2025
08c4160
progress???
CvX May 27, 2025
86b6398
sourcemaps?
CvX May 27, 2025
ece92a2
log error?
CvX May 27, 2025
46a624a
context
davidtaylorhq Jun 3, 2025
27f7bab
sourcemaps
davidtaylorhq Jun 3, 2025
03c5a13
bump
davidtaylorhq Jun 3, 2025
c2c647d
bump
davidtaylorhq Jun 3, 2025
2292300
bump
davidtaylorhq Jun 3, 2025
e44feb2
connector handling?
davidtaylorhq Jun 3, 2025
09854b5
Merge branch 'main' into rollup-theme-experiment
davidtaylorhq Jun 6, 2025
22e339c
Revert "connector handling?"
CvX Jun 10, 2025
121af98
dead code
CvX Jun 10, 2025
c2a147a
no log
CvX Jun 10, 2025
9c7761d
log/lint
CvX Jun 10, 2025
6d82079
update the connector spec
CvX Jun 10, 2025
fd06df9
connector logic
CvX Jun 10, 2025
6eb7c14
return
CvX Jun 10, 2025
789c84e
update spec
CvX Jun 10, 2025
2c806f7
Merge branch 'main' into 0-a-rollup-theme-experiment
CvX Jun 10, 2025
f0e5e65
fix name replacement
CvX Jun 10, 2025
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
6 changes: 6 additions & 0 deletions app/assets/javascripts/discourse/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import { buildResolver } from "discourse/resolver";
const _pluginCallbacks = [];
let _unhandledThemeErrors = [];

window.moduleBroker = {
lookup: function (moduleName) {
return require(moduleName);
},
};

class Discourse extends Application {
modulePrefix = "discourse";
rootElement = "#main";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
document.addEventListener("discourse-init", (e) => {
document.addEventListener("discourse-init", async (e) => {
performance.mark("discourse-init");
const config = e.detail;
const app = require(`${config.modulePrefix}/app`)["default"].create(config);
const klass = require(`${config.modulePrefix}/app`).default;

for (const link of document.querySelectorAll("link[rel=modulepreload]")) {
const themeId = link.dataset.themeId;
const compatModules = (await import(link.href)).default;
for (const [key, mod] of Object.entries(compatModules)) {
define(`discourse/theme-${themeId}/${key}`, () => mod);
}
}

const app = klass.create(config);
app.start();
});
52 changes: 52 additions & 0 deletions app/assets/javascripts/theme-transpiler/add-theme-globals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export default function (babel) {
const { types: t } = babel;

const visitor = {
Program(path) {
const importDeclarations = [];

if (path.scope.bindings.themePrefix) {
if (path.scope.bindings.themePrefix.kind !== "module") {
throw new Error("duplicate themePrefix");
} else {
// TODO: maybe check the import path
}
} else {
importDeclarations.push(
t.importSpecifier(
t.identifier("themePrefix"),
t.identifier("themePrefix")
)
);
}

if (path.scope.bindings.settings) {
if (path.scope.bindings.settings.kind !== "module") {
throw new Error("duplicate settings");
} else {
// TODO: maybe check the import path
}
} else {
importDeclarations.push(
t.importSpecifier(t.identifier("settings"), t.identifier("settings"))
);
}

if (importDeclarations.length > 0) {
path.node.body.push(
t.importDeclaration(
importDeclarations,
t.stringLiteral("virtual:theme")
)
);
}
},
};

return {
pre(file) {
babel.traverse(file.ast, visitor, file.scope);
file.scope.crawl();
},
};
}
50 changes: 50 additions & 0 deletions app/assets/javascripts/theme-transpiler/babel-replace-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import rollupVirtualImports from "./rollup-virtual-imports";

export default function (babel) {
const { types: t } = babel;

return {
visitor: {
ImportDeclaration(path) {
const moduleName = path.node.source.value;
if (moduleName.startsWith(".") || rollupVirtualImports[moduleName]) {
return;
}

const properties = path.node.specifiers.map((specifier) => {
if (specifier.type === "ImportDefaultSpecifier") {
return t.objectProperty(
t.identifier("default"),
t.identifier(specifier.local.name)
);
} else {
return t.objectProperty(
t.identifier(specifier.imported.name),
t.identifier(specifier.local.name)
);
}
});

const replacement = t.variableDeclaration("const", [
t.variableDeclarator(
t.objectPattern(properties),
t.awaitExpression(
t.callExpression(
t.memberExpression(
t.memberExpression(
t.identifier("window"),
t.identifier("moduleBroker")
),
t.identifier("lookup")
),
[t.stringLiteral(moduleName)]
)
)
),
]);

path.replaceWith(replacement);
},
},
};
}
5 changes: 5 additions & 0 deletions app/assets/javascripts/theme-transpiler/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ esbuild
path: "path-browserify",
url: "./url-polyfill",
"source-map-js": "source-map-js",
assert: "./noop",
fs: "./noop",
},
banner: {
js: `var process = { "env": { "EMBER_ENV": "production" }, "cwd": () => "/" };`,
},
define: {
"import.meta.url": "'http://example.com'",
},
external: [],
entryPoints: ["./transpiler.js"],
plugins: [wasmPlugin],
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/theme-transpiler/noop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
14 changes: 12 additions & 2 deletions app/assets/javascripts/theme-transpiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
"license": "GPL-2.0-only",
"keywords": [],
"dependencies": {
"@babel/preset-env": "^7.27.2",
"@babel/standalone": "^7.27.6",
"@csstools/postcss-light-dark-function": "^2.0.9",
"@rollup/browser": "^4.40.2",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/wasm-node": "^4.29.1",
"@swc/wasm-web": "^1.10.4",
"@zxing/text-encoding": "^0.9.0",
"autoprefixer": "^10.4.21",
"babel-plugin-ember-template-compilation": "^2.4.1",
"content-tag": "^4.0.0",
Expand All @@ -20,13 +26,17 @@
"ember-source": "~5.12.0",
"ember-this-fallback": "^0.4.0",
"fastestsmallesttextencoderdecoder": "^1.0.22",
"jsdom": "^25.0.1",
"magic-string": "^0.30.17",
"path-browserify": "^1.0.1",
"polyfill-crypto.getrandomvalues": "^1.0.0",
"postcss": "^8.5.4",
"postcss-js": "^4.0.1",
"postcss-media-minmax": "^5.0.0",
"postcss": "^8.5.4",
"source-map-js": "^1.2.1",
"terser": "^5.42.0"
"terser": "^5.42.0",
"url-polyfill": "^1.1.12",
"url-shim": "^1.0.1"
},
"engines": {
"node": ">= 18",
Expand Down
59 changes: 59 additions & 0 deletions app/assets/javascripts/theme-transpiler/rollup-virtual-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export default {
"virtual:main": (tree) => {
let output = `
import "virtual:init-settings";
`;

output += "const themeCompatModules = {};\n";

let i = 1;
for (const moduleFilename of Object.keys(tree)) {
if (moduleFilename.match(/(^|\/)connectors\//)) {
let moduleName = moduleFilename.replace(/\.es6?$/, "");

const isTemplate = moduleName.endsWith(".hbs");
const isInTemplatesDirectory = moduleName.match(/(^|\/)templates\//);

moduleName = moduleName.replace(/\.[^\.]+$/, "");

if (isTemplate && !isInTemplatesDirectory) {
moduleName = moduleName.replace(
/(^|\/)connectors\//,
"$1templates/connectors/"
);
} else if (!isTemplate && isInTemplatesDirectory) {
moduleName = moduleName.replace(/^templates\//, "");
}
output += `import * as Mod${i} from "${moduleFilename}";\n`;
output += `themeCompatModules["${moduleName}"] = Mod${i};\n`;
} else {
const moduleName = moduleFilename.replace(/\.[^\.]+(\.es6)?$/, "");
output += `import * as Mod${i} from "${moduleName}";\n`;
output += `themeCompatModules["${moduleName}"] = Mod${i};\n`;
}

i += 1;
}

output += "export default themeCompatModules;\n";

return output;
},
"virtual:init-settings": (_, { themeId, settings }) => {
return `
import { registerSettings } from "discourse/lib/theme-settings-store";
registerSettings(${themeId}, ${JSON.stringify(settings)});
`;
},
"virtual:theme": (_, { themeId }) => {
return `
import { getObjectForTheme } from "discourse/lib/theme-settings-store";

export const settings = getObjectForTheme(${themeId});

export function themePrefix(key) {
return \`theme_translations.${themeId}.\${key}\`;
}
`;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function manipulateAstNodeForTheme(node, themeId) {
// Magically add theme id as the first param for each of these helpers)
if (
node.path.parts &&
["theme-i18n", "theme-prefix", "theme-setting"].includes(node.path.parts[0])
) {
if (node.params.length === 1) {
node.params.unshift({
type: "NumberLiteral",
value: themeId,
original: themeId,
loc: { start: {}, end: {} },
});
}
}
}

export default function buildEmberTemplateManipulatorPlugin(themeId) {
return function () {
return {
name: "theme-template-manipulator",
visitor: {
SubExpression: (node) => manipulateAstNodeForTheme(node, themeId),
MustacheStatement: (node) => manipulateAstNodeForTheme(node, themeId),
},
};
};
}
Loading
Loading