1
0
mirror of https://github.com/ONLYOFFICE/DocSpace-client.git synced 2026-10-23 13:25:44 +03:00
Files
DocSpace-client/scripts/ui-kit-dev.cjs
Alexey Safronov 44367892de Serve ui-kit from a checkout in the Next apps too, not just the client
`pnpm run start:ui-kit-src` started all five apps but only the client
served ui-kit from the checkout; login, doceditor, management and sdk
kept using the installed tarball, so editing ui-kit still meant build,
pack, install and a restart for four of the five.

They now honour DOCSPACE_UI_KIT_SRC through scripts/ui-kit-dev.cjs, which
each next.config.js calls twice: once outside the webpack hook to refuse
a production build while the variable is set, and once inside it to

- alias @onlyoffice/apps-ui-kit to the checkout,
- alias every peer the checkout declares to the app's own copy, so a
  package both trees carry does not end up loaded twice (two Reacts, two
  ThemeContexts), and
- add the checkout to the next-swc-loader rules, which is what makes its
  TypeScript compile: Next runs its loaders over the app's own tree, and
  a checkout outside it is not part of that.

The CSS needed nothing. Next's rules key off the extension, so the
checkout's *.module.scss go through the app's CSS Modules pipeline and
come out named by it -- `.AsideHeader_container__gUShG` where the package
would ship `dsui-AsideHeader__container--<hash>`. That is the same trade
the client already makes in this mode, and it is why a build from a
checkout is refused rather than merely discouraged.

Verified by running it: all five dev servers up, each of the four Next
apps compiling ui-kit from the checkout (40 to 54 files of their dev
output reference it), `next build` refusing with the variable set, and a
normal `next build` with it unset producing output that references the
checkout nowhere. tsc, Biome and the common checks pass.

The escaping-import guard the client has (config/plugins/ui-kit-boundary)
has no Next equivalent yet; such an import fails later, in ui-kit's own
build. Noted in the rule.
2026-09-17 12:49:45 +04:00

199 lines
6.7 KiB
JavaScript

/*
* Copyright (C) Ascensio System SIA, 2009-2026
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation, together with the
* additional terms provided in the LICENSE file.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: https://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA by email at info@onlyoffice.com
* or by postal mail at 20A-6 Ernesta Birznieka-Upisha Street, Riga,
* LV-1050, Latvia, European Union.
*
* The interactive user interfaces in modified versions of the Program
* are required to display Appropriate Legal Notices in accordance with
* Section 5 of the GNU AGPL version 3.
*
* No trademark rights are granted under this License.
*
* All non-code elements of the Product, including illustrations,
* icon sets, and technical writing content, are licensed under the
* Creative Commons Attribution-ShareAlike 4.0 International License:
* https://creativecommons.org/licenses/by-sa/4.0/legalcode
*
* This license applies only to such non-code elements and does not
* modify or replace the licensing terms applicable to the Program's
* source code, which remains licensed under the GNU Affero General
* Public License v3.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// Serving @onlyoffice/apps-ui-kit from a checkout in the Next apps, the way
// packages/client/config/ui-kit-dev.ts does it for Vite.
//
// Waiting for build, pack, install and a restart on every ui-kit edit is the
// cost of consuming a prebuilt package. DOCSPACE_UI_KIT_SRC removes it for
// local work: the package specifier is aliased to the checkout and webpack
// compiles its TypeScript and SCSS like the app's own.
//
// CommonJS on purpose -- a next.config.js is loaded as CommonJS.
const fs = require("fs");
const path = require("path");
const UI_KIT_PACKAGE = "@onlyoffice/apps-ui-kit";
const UI_KIT_SRC_ENV = "DOCSPACE_UI_KIT_SRC";
const fail = (message) => {
throw new Error(`${UI_KIT_SRC_ENV}: ${message}`);
};
/**
* The checkout to serve, or null when the variable is unset.
* `appDir` is the app's own directory; the path is resolved against the repo
* root, so the same value works for every app and for the Vite client.
*/
const readCheckout = (appDir) => {
const raw = process.env[UI_KIT_SRC_ENV]?.trim();
if (!raw) return null;
const root = path.resolve(appDir, "../..", raw);
const manifestPath = path.join(root, "package.json");
if (!fs.existsSync(root))
fail(`${root} does not exist. Point it at a ${UI_KIT_PACKAGE} checkout.`);
if (!fs.existsSync(manifestPath))
fail(`${root} has no package.json, so it is not a ${UI_KIT_PACKAGE} checkout.`);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
if (manifest.name !== UI_KIT_PACKAGE)
fail(`${root} is "${manifest.name}", not ${UI_KIT_PACKAGE}.`);
if (!fs.existsSync(path.join(root, "node_modules")))
fail(
`${root} has no node_modules. The checkout resolves its own dependencies, ` +
"so run `pnpm install` there first.",
);
return { root, manifest };
};
/** Every rule whose loader is next-swc-loader, at any depth of `oneOf`. */
const swcRules = (rules, found = []) => {
for (const rule of rules ?? []) {
if (!rule || typeof rule !== "object") continue;
if (rule.oneOf) swcRules(rule.oneOf, found);
const loaders = Array.isArray(rule.use)
? rule.use
: rule.use
? [rule.use]
: [];
if (
loaders.some(
(loader) =>
typeof loader === "object" &&
typeof loader.loader === "string" &&
loader.loader.includes("next-swc-loader"),
)
) {
found.push(rule);
}
}
return found;
};
/**
* Points an app's webpack config at a ui-kit checkout. A no-op when the
* variable is unset, so every next.config.js can call it unconditionally.
* Returns the checkout path, for the log line.
*/
const applyUiKitSourceMode = (config, appDir) => {
const checkout = readCheckout(appDir);
if (!checkout) return null;
const { root, manifest } = checkout;
config.resolve = config.resolve ?? {};
config.resolve.alias = { ...config.resolve.alias, [UI_KIT_PACKAGE]: root };
// The checkout sits outside this repo, so every bare specifier in ui-kit
// source resolves against the checkout's own node_modules. For a package
// both trees carry that means two copies, and for anything holding module
// state -- a context, a store, a socket -- two copies is a silent behaviour
// change. Aliasing the package directory keeps subpath imports working
// (react-dom/client, and the deep imports the AI stack uses).
for (const peer of Object.keys(manifest.peerDependencies ?? {})) {
if (peer === UI_KIT_PACKAGE) continue;
try {
config.resolve.alias[peer] = path.dirname(
require.resolve(`${peer}/package.json`, { paths: [appDir] }),
);
} catch {
try {
// No package.json export: fall back to pinning the entry point only.
config.resolve.alias[`${peer}$`] = require.resolve(peer, {
paths: [appDir],
});
} catch {
// Not installed here at all; the checkout resolves it itself.
}
}
}
// Next compiles what lies under the app, and the checkout does not. Adding
// it to the SWC rules is what makes its TypeScript compile; the CSS rules
// need nothing -- they key off the file extension, so the checkout's
// *.module.scss go through the app's own CSS Modules pipeline and come out
// named by it rather than by ui-kit's rollup config.
const rules = swcRules(config.module?.rules);
if (rules.length === 0)
fail(
"no next-swc-loader rule found in the webpack config, so ui-kit source " +
"would not compile. Next's internals changed; update scripts/ui-kit-dev.cjs.",
);
for (const rule of rules) {
if (Array.isArray(rule.include)) rule.include.push(root);
}
return root;
};
/**
* Refuses a production build while the variable is set. The alias is a
* dev-server switch: a build from a checkout is not the artifact the package
* produces -- it is compiled by the app's own loaders, with the app's CSS
* Modules naming, and none of ui-kit's own build steps run.
*/
const refuseBuildFromSource = (appDir) => {
const checkout = readCheckout(appDir);
if (checkout)
fail(
`set to ${checkout.root}, and it is a dev-server switch. ` +
"Unset it to build: a build must come from the installed package.",
);
};
module.exports = {
UI_KIT_PACKAGE,
UI_KIT_SRC_ENV,
applyUiKitSourceMode,
refuseBuildFromSource,
};