​На прошлой неделе новая версия 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 открывает новые возможности, особенно некоторые идеи, которые у нас есть о маршрутизации, но я, вероятно, расскажу об этом подробнее в другом посте в блоге 😉.

​В бесконечность и дальше
Дэвид​

Чтобы узнать больше о приключениях, подписывайтесь на меня в Твиттере 🖖