#!/usr/bin/env node // // This file is part of Canvas. // Copyright (C) 2026-present Instructure, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // /* yarn build-instui Downloads design tokens from the instructure-ui repository (pinned to INSTUI_VERSION) and generates the SwiftUI source files for the InstUI Swift package. Generated files (DO NOT EDIT manually): Primitives: packages/InstUI/Sources/Primitive/Generated/InstUI.Primitive.Color.swift packages/InstUI/Sources/Primitive/Generated/InstUI.Primitive.Size.swift packages/InstUI/Sources/Primitive/Generated/InstUI.Primitive.FontWeight.swift packages/InstUI/Sources/Primitive/Generated/InstUI.Primitive.FontFamily.swift packages/InstUI/Sources/Primitive/Generated/InstUI.Primitive.Opacity.swift Semantic Swift types: packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.Color.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.Size.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.Spacing.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.BorderRadius.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.BorderWidth.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.FontSize.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.Opacity.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.FontWeight.swift packages/InstUI/Sources/Semantic/Generated/InstUI.Semantic.FontFamily.swift Component Swift types (one file per component, sync'd from upstream): packages/InstUI/Sources/Component/Generated/InstUI.Component..swift Component collector (generated from the component list): packages/InstUI/Sources/Component/Generated/InstUI.Theme.Components.swift Token values (bundled JSON): packages/InstUI/Resources/Tokens/Semantic/Color/rebrandLight.json packages/InstUI/Resources/Tokens/Semantic/Color/rebrandDark.json packages/InstUI/Resources/Tokens/Semantic/Layout/default.json packages/InstUI/Resources/Tokens/Component/.json To update to a newer version of instructure-ui, bump INSTUI_VERSION below and re-run. Components are sync'd automatically: the Generated/ directory is wiped and rebuilt on each run, so any component removed upstream will also be removed from this repo. */ const https = require('https') const fs = require('fs') const path = require('path') const buildPrimitivesConfig = require('./sd.config.primitives') const buildSemanticConfig = require('./sd.config.semantic') const { buildComponent: buildComponentConfig } = require('./sd.config.component') const { buildIcons } = require('./build-icons') const INSTUI_VERSION = 'v11.7.1' const TOKENS_BASE_URL = `https://raw.githubusercontent.com/instructure/instructure-ui/${INSTUI_VERSION}/packages/ui-scripts/lib/build/tokensStudio` const DOWNLOAD_TIMEOUT_MS = 30_000 function download(url) { return new Promise((resolve, reject) => { const req = https.get(url, res => { let data = '' res.on('data', chunk => { data += chunk }) res.on('end', () => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode} for ${url}`)) } else { resolve(data) } }) res.on('error', reject) }) req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => { req.destroy(new Error(`Timed out after ${DOWNLOAD_TIMEOUT_MS}ms: ${url}`)) }) req.on('error', reject) }) } function apiGet(url) { return new Promise((resolve, reject) => { const req = https.get(url, { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'build-instui' } }, res => { let data = '' res.on('data', chunk => { data += chunk }) res.on('end', () => { if (res.statusCode !== 200) { reject(new Error(`GitHub API HTTP ${res.statusCode} for ${url}`)) } else { resolve(JSON.parse(data)) } }) res.on('error', reject) }) req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => { req.destroy(new Error(`Timed out after ${DOWNLOAD_TIMEOUT_MS}ms: ${url}`)) }) req.on('error', reject) }) } async function buildPrimitives() { const url = `${TOKENS_BASE_URL}/primitives/default.json` console.log('Downloading primitive tokens...') const primitives = JSON.parse(await download(url)) console.log('Building SwiftUI primitives...') buildPrimitivesConfig(primitives).buildAllPlatforms() } async function buildSemantic() { console.log('Downloading semantic tokens...') const [light, dark, layout] = await Promise.all([ download(`${TOKENS_BASE_URL}/rebrand/semantic/color/rebrandLight.json`), download(`${TOKENS_BASE_URL}/rebrand/semantic/color/rebrandDark.json`), download(`${TOKENS_BASE_URL}/rebrand/semantic/layout/default.json`), ]) const resourcesDir = path.join(__dirname, '../../packages/InstUI/Resources') const tokensDir = path.join(resourcesDir, 'Tokens') const versionMarker = `InstUI_${INSTUI_VERSION.replace(/\./g, '_')}` for (const entry of fs.readdirSync(resourcesDir)) { if (entry.startsWith('InstUI_v') && entry !== versionMarker) { fs.rmSync(path.join(resourcesDir, entry)) console.log(`Removed old version marker: ${entry}`) } } fs.writeFileSync(path.join(resourcesDir, versionMarker), '') const colorDir = path.join(tokensDir, 'Semantic', 'Color') const layoutDir = path.join(tokensDir, 'Semantic', 'Layout') fs.rmSync(tokensDir, { recursive: true, force: true }) fs.mkdirSync(colorDir, { recursive: true }) fs.mkdirSync(layoutDir, { recursive: true }) fs.writeFileSync(path.join(colorDir, 'rebrandLight.json'), light) fs.writeFileSync(path.join(colorDir, 'rebrandDark.json'), dark) fs.writeFileSync(path.join(layoutDir, 'default.json'), layout) console.log(`Saved color + layout tokens to Resources/Tokens/Semantic/`) console.log('Building SwiftUI semantic types...') buildSemanticConfig(JSON.parse(light), JSON.parse(dark), JSON.parse(layout)) return tokensDir } async function buildComponents(tokensDir) { const apiUrl = `https://api.github.com/repos/instructure/instructure-ui/contents/packages/ui-scripts/lib/build/tokensStudio/rebrand/component?ref=${INSTUI_VERSION}` console.log('Fetching component token file listing...') const entries = await apiGet(apiUrl) const jsonFiles = entries.filter(e => e.type === 'file' && e.name.endsWith('.json')) console.log(`Downloading ${jsonFiles.length} component token files...`) const downloads = await Promise.all(jsonFiles.map(e => download(e.download_url).then(text => ({ name: e.name, text })))) const componentDir = path.join(tokensDir, 'Component') fs.mkdirSync(componentDir, { recursive: true }) for (const { name, text } of downloads) { fs.writeFileSync(path.join(componentDir, name), text) } console.log(`Saved ${downloads.length} component JSONs to Resources/${path.basename(tokensDir)}/Component/`) const outputDir = path.join(__dirname, '../../packages/InstUI/Sources/Component/Generated') console.log('Building SwiftUI component types...') buildComponentConfig(componentDir, outputDir) console.log(`Generated ${downloads.length} component Swift files.`) } async function main() { await buildIcons(INSTUI_VERSION) await buildPrimitives() const tokensDir = await buildSemantic() await buildComponents(tokensDir) console.log('Done.') } main().catch(err => { console.error(err) process.exit(1) })