На прошлой неделе новая версия NNS-dapp (децентрализованное приложение NNS, одного из крупнейших в мире DAO, управляющего Интернет-компьютером) представила новую функцию под названием Stake Maturity, облегченное обновление дизайна его модальных окон. и изменение системы сборки.
Действительно, в то время как внешнее приложение раньше упаковывалось с помощью единственного сборщика Rollup, оно было перенесено на SvelteKit*, который использует как Vite, esbuild, так и Rollup.
Вот три вещи, которые я усвоил на этом пути. Я надеюсь, что они будут вам полезны, чтобы вы могли безопасно развертывать свои приложения в рабочей среде.
* без каких-либо изменений в отношении маршрутизации, пока
1. CSP ломает приложение в Firefox
Политика безопасности контента (CSP) — это дополнительный уровень безопасности, который помогает обнаруживать и смягчать определенные типы атак, включая межсайтовый скриптинг (XSS) и атаки с внедрением данных (источник MDN).
Поскольку мы заботимся о безопасности, конечно, мы внедрили такие типы правил. В частности, политики script-src
, которые заносят в белый список теги скриптов страницы index.html
, используя их хэши скриптов sha256
, и 'script-dynamic'
, которые используются для загрузки всех фрагментов кода, необходимых для запуска приложения в браузере.
В то время как это работало с предыдущим сборщиком, мы были удивлены, обнаружив, что SvelteKit (октябрь 2022 г.) на самом деле не поддерживает такое сочетание политик (см. issue #3558). Он работает в Chrome и Safari, но ломается в Firefox, а слово перерыв означает, что все приложение вообще не будет отображаться.
Чтобы решить эту проблему, мы нашли следующий обходной путь: добавление скрипта после сборки, который извлекает в отдельный JS-файл код, внедренный в HTML-страницу SvelteKit, и вместо этого внедряет наш собственный загрузчик скриптов 🤪.
Это может быть достигнуто следующим образом:
1. добавьте пустой main.js
в папку static
(полезно, чтобы избежать проблем при локальной разработке).
2. добавить загрузчик скриптов в <head />
корневой html-страницы, т.е. на страницу src/app.html
.
<script> const loader = document.createElement("script"); loader.type = "module"; loader.src = "./main.js"; document.head.appendChild(loader); </script>
3. создать сценарий после сборки — например. ./scripts/build.csp.mjs
.
#!/usr/bin/env node import { readFileSync, writeFileSync } from "fs"; import { join } from "path"; const publicIndexHTML = join(process.cwd(), "public", "index.html"); const buildCsp = () => { const indexHTMLWithoutStartScript = extractStartScript(); writeFileSync(publicIndexHTML, indexHTMLWithoutStartScript); }; /** * Using a CSP with 'strict-dynamic' with SvelteKit breaks in Firefox. * Issue: https://github.com/sveltejs/kit/issues/3558 * * As workaround: * 1. we extract the start script that is injected by SvelteKit in index.html into a separate main.js * 2. we remove the script content from index.html but, let the script tag as anchor * 3. we use our custom script loader to load the main.js script */ const extractStartScript = () => { const indexHtml = readFileSync(publicIndexHTML, "utf-8"); const svelteKitStartScript = /(<script type=\"module\" data-sveltekit-hydrate[\s\S]*?>)([\s\S]*?)(<\/script>)/gm; // 1. extract SvelteKit start script to a separate main.js file const [_script, _scriptStartTag, content, _scriptEndTag] = svelteKitStartScript.exec(indexHtml); const inlineScript = content.replace(/^\s*/gm, ""); writeFileSync( join(process.cwd(), "public", "main.js"), inlineScript, "utf-8" ); // 2. replace SvelteKit script tag content with empty return indexHtml.replace(svelteKitStartScript, "$1$3"); }; buildCsp();
4. связать сценарий в package.json
.
{ "scripts": { "build:csp": "node scripts/build.csp.mjs", "build": "vite build && npm run build:csp" } }
2. Обеспечьте воспроизводимость
Воспроизводимые сборки — это процесс компиляции программного обеспечения, обеспечивающий возможность воспроизведения полученного бинарного кода (источник википедия). Мы заботимся о детерминированной компиляции, потому что хотим обеспечить проверку того, что в процессе компиляции не было введено никаких уязвимостей или бэкдоров.
Это всегда срабатывало как волшебство. Однако после миграции мы больше не могли вычислять один и тот же sha для связанного wasm на нескольких компьютерах.
После некоторой отладки мы обнаружили две основные причины проблемы.
1. если для SvelteKit не указана какая-либо конкретная версия, вместо этого он сгенерирует отметку времени для идентификации текущей версии приложения — т. е. если версия не предоставлена, SvelteKit вставит отметку времени в код JS, который будет связан. Каждый билд, каждый раз новая метка времени.
Чтобы решить эту проблему, мы прочитали номер версии в package.json
и предоставили его набору в svelte.config.js
. Таким образом, версия становится статической для каждой сборки до тех пор, пока мы не нарушаем семантические числа.
import adapter from "@sveltejs/adapter-static"; import autoprefixer from "autoprefixer"; import { readFileSync } from "fs"; import preprocess from "svelte-preprocess"; import { fileURLToPath } from "url"; const file = fileURLToPath(new URL("package.json", import.meta.url)); const json = readFileSync(file, "utf8"); const { version } = JSON.parse(json); const config = { preprocess: preprocess({ postcss: { plugins: [autoprefixer], }, }), kit: { adapter: adapter({ pages: "public", assets: "public", fallback: "index.html", precompress: false, }), serviceWorker: { register: false, }, version: { name: version, // <---- here provide version }, trailingSlash: "always", }, }; export default config;
2. SvelteKit — или Vite — добавляет файл public/vite-manifest.json
, который содержит список всех сгенерированных неизменяемых ресурсов приложения. К сожалению, этот файл в настоящее время не отсортирован. Поэтому в качестве быстрого исправления мы добавили для этого скрипт bash.
#!/usr/bin/env bash set -euxo pipefail cd "$(dirname "$(realpath "$0")")/.." # shellcheck disable=SC2094 # This reads the entire file into memory and then writes it out, so is correct. cat <<<"$(jq --sort-keys . public/vite-manifest.json)" >public/vite-manifest.json
Скрипт Bash, который мы также прицепили к package.json
.
{ "scripts": { "build:csp": "node scripts/build.csp.mjs", "build": "vite build && npm run build:csp && ./scripts/make-reproducible" } }
3. Буфер полифилла
Я всегда использовал решение SO Чови для полифилла Buffer API для интерфейсных децентрализованных приложений на IC, но это больше не срабатывало. Хотя переопределение global
как globalThis
в vite.config.js
по-прежнему работало, полифилл для буфера не применялся.
Именно поэтому мы добавили ручной полифилл в корень +layout.ts
после установки (npm i buffer
) зависимости модуля buffer для браузера.
import { Buffer } from "buffer"; globalThis.Buffer = Buffer;
Тем не менее, мы обнаружили, что это работает так, как задумано локально, в сборке разработки или рабочей версии, но может стать проблемой в рабочей среде, поскольку нет гарантии, что файл +layout.js
будет загружен быстрее, чем страницы, которые его используют.
Вот почему в дополнение к вышеперечисленным надстройкам стоит внедрить полифицированный буфер в производственный JS-код, который поставляется в комплекте. Это можно сделать с помощью плагина Rollup (npm i @rollup/plugin-inject -D
).
import inject from "@rollup/plugin-inject"; import { sveltekit } from "@sveltejs/kit/vite"; import type { UserConfig } from "vite"; const config: UserConfig = { plugins: [sveltekit()], build: { target: "es2020", rollupOptions: { // Polyfill Buffer for production build. // The hardware wallet needs Buffer. plugins: [ inject({ include: ["node_modules/@ledgerhq/**"], modules: { Buffer: ["buffer", "Buffer"] }, }), ], }, }, optimizeDeps: { esbuildOptions: { // Node.js global to browser globalThis define: { global: "globalThis", }, }, }, }; export default config;
Примечания:
- нам нужен вышеупомянутый полифилл для функций, связанных с аппаратным кошельком. Вот почему мы ограничиваем его библиотекой
ledgerhq
при использовании плагина Rollup. - решение пока не является оптимальным, потому что мы применяем полифилл дважды — т. е. фактически загружаем слишком много кода JavaScript в рабочей сборке. Никогда не слишком уверен, я согласен, но, тем не менее, это можно улучшить.
- код веб-работника не подвергается полифии с вышеуказанным решением. Если вам нужно это сделать, вам, вероятно, потребуется провести дальнейшее расследование.
Заключение
Это все веселье и игры, пока вы не обнаружите проблемы, которых не существует при локальной разработке 😁. Я рад, что мы решили все эти проблемы и смогли мигрировать. Использование ViteJS упрощает работу разработчиков, а перенос децентрализованного приложения на SvelteKit открывает новые возможности, особенно некоторые идеи, которые у нас есть о маршрутизации, но я, вероятно, расскажу об этом подробнее в другом посте в блоге 😉.
В бесконечность и дальше
Дэвид
Чтобы узнать больше о приключениях, подписывайтесь на меня в Твиттере 🖖