diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8bb0c79..311323b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,25 +76,17 @@ jobs: with: {repo: sass/embedded-protocol, default-ref: null} - name: Check out Dart Sass - id: clone-dart-sass uses: sass/clone-linked-repo@v1 - with: {repo: sass/dart-sass, default-ref: null} + with: {repo: sass/dart-sass} - name: Check out the embedded compiler uses: sass/clone-linked-repo@v1 - with: - repo: sass/dart-sass-embedded - # If we check out a specific version of Dart Sass, always check out - # the embedded compiler as well so we can actually use that Dart Sass - # version. - default-ref: ${{ !steps.clone-dart-sass.skip && 'main' || null }} + with: {repo: sass/dart-sass-embedded} - name: Link the embedded compiler to Dart Sass run: | - if [[ -d dart-sass ]]; then - yq -i '.dependency_overrides.sass = {"path": "../dart-sass"}' \ - dart-sass-embedded/pubspec.yaml - fi + yq -i '.dependency_overrides.sass = {"path": "../dart-sass"}' \ + dart-sass-embedded/pubspec.yaml - name: Check out the JS API definition uses: sass/clone-linked-repo@v1 @@ -104,8 +96,7 @@ jobs: - name: npm run init run: | if [[ -d embedded-protocol ]]; then args=--protocol-path=embedded-protocol; fi - if [[ -d dart-sass-embedded ]]; then args="$args --compiler-path=dart-sass-embedded"; fi - npm run init -- --api-path=language $args + npm run init -- --compiler-path=dart-sass-embedded --api-path=language $args - run: npm run test @@ -146,25 +137,17 @@ jobs: with: {repo: sass/embedded-protocol, default-ref: null} - name: Check out Dart Sass - id: clone-dart-sass uses: sass/clone-linked-repo@v1 - with: {repo: sass/dart-sass, default-ref: null} + with: {repo: sass/dart-sass} - name: Check out the embedded compiler uses: sass/clone-linked-repo@v1 - with: - repo: sass/dart-sass-embedded - # If we check out a specific version of Dart Sass, always check out - # the embedded compiler as well so we can actually use that Dart Sass - # version. - default-ref: ${{ !steps.clone-dart-sass.skip && 'main' || null }} + with: {repo: sass/dart-sass-embedded} - name: Link the embedded compiler to Dart Sass run: | - if [[ -d dart-sass ]]; then - yq -i '.dependency_overrides.sass = {"path": "../dart-sass"}' \ - dart-sass-embedded/pubspec.yaml - fi + yq -i '.dependency_overrides.sass = {"path": "../dart-sass"}' \ + dart-sass-embedded/pubspec.yaml - name: Check out the JS API definition uses: sass/clone-linked-repo@v1 @@ -174,8 +157,7 @@ jobs: - name: npm run init run: | if [[ -d embedded-protocol ]]; then args=--protocol-path=embedded-protocol; fi - if [[ -d dart-sass-embedded ]]; then args="$args --compiler-path=dart-sass-embedded"; fi - npm run init -- --api-path=language $args + npm run init -- --compiler-path=dart-sass-embedded --api-path=language $args - name: Check out sass-spec uses: sass/clone-linked-repo@v1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4199a97e..20e63a3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,30 +67,18 @@ JS API): * `---ref`: A Git reference for the GitHub repository of the package to clone. -* `---version`: The released version of the package to use. This isn't - available for `api`, because the JS API definition doesn't have its own tagged - versions. - By default: * This uses the version of the embedded protocol and compiler specified by - `protocol-version` and `compiler-version` in `package.json`, *unless* these - versions end in `-dev` in which case it checks out the latest revision on - GitHub. - -* This uses the JS API definition from the latest revision on GitHub. - -`npm run init` chooses the Dart Sass version as follows: - -* If a released version of the embedded compiler was specified, it uses the - version of Dart Sass compiled into that release. + `protocol-version` in `package.json`, *unless* that version ends in `-dev` in + which case it checks out the latest revision on GitHub. -* If `--compiler-path` was specified, it uses the version of Dart Sass linked to - that compiler. +* This uses the embedded compiler version and JS API definition from the latest + revision on GitHub. -* If the embedded compiler was cloned from GitHub, it uses the version of Dart - Sass specified in its pubspec *unless* that version ends in `-dev`, in which - case it checks out the latest revision on GitHub. +* This uses the Dart Sass version from the latest revision on GitHub, unless the + embedded `--compiler-path` was passed in which case it uses whatever version + of Dart Sass that package references. ## Continuous Integration diff --git a/package.json b/package.json index 642335eb..b1bcae60 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "node": ">=14.0.0" }, "scripts": { - "init": "ts-node ./tool/prepare-dev-environment.ts", + "init": "ts-node ./tool/init.ts", "check": "npm-run-all check:gts check:tsc", "check:gts": "gts check", "check:tsc": "tsc --noEmit", diff --git a/tool/prepare-dev-environment.ts b/tool/init.ts similarity index 59% rename from tool/prepare-dev-environment.ts rename to tool/init.ts index f21c2f1d..2106a62e 100644 --- a/tool/prepare-dev-environment.ts +++ b/tool/init.ts @@ -4,13 +4,9 @@ import yargs from 'yargs'; -import { - nodeArchToDartArch, - getDartSassEmbedded, - getEmbeddedProtocol, - getJSApi, - nodePlatformToDartPlatform, -} from './utils'; +import {getEmbeddedCompiler} from './get-embedded-compiler'; +import {getEmbeddedProtocol} from './get-embedded-protocol'; +import {getJSApi} from './get-js-api'; const argv = yargs(process.argv.slice(2)) .option('compiler-path', { @@ -22,10 +18,6 @@ const argv = yargs(process.argv.slice(2)) type: 'string', description: 'Build the Embedded Dart Sass binary from this Git ref.', }) - .option('compiler-version', { - type: 'string', - description: 'Download this version of the Embedded Dart Sass binary.', - }) .option('skip-compiler', { type: 'boolean', description: "Don't Embedded Dart Sass at all.", @@ -38,10 +30,6 @@ const argv = yargs(process.argv.slice(2)) type: 'string', description: 'Build the Embedded Protocol from this Git ref.', }) - .option('protocol-version', { - type: 'string', - description: 'Build the Embedded Protocol from this release version.', - }) .option('api-path', { type: 'string', description: 'Use the JS API definitions from the source at this path.', @@ -51,11 +39,9 @@ const argv = yargs(process.argv.slice(2)) description: 'Build the JS API definitions from this Git ref.', }) .conflicts({ - 'compiler-path': ['compiler-ref', 'compiler-version', 'skip-compiler'], - 'compiler-ref': ['compiler-version', 'skip-compiler'], - 'compiler-version': 'skip-compiler', - 'protocol-path': ['protocol-ref', 'protocol-version'], - 'protocol-ref': 'protocol-version', + 'compiler-path': ['compiler-ref', 'skip-compiler'], + 'compiler-ref': ['skip-compiler'], + 'protocol-path': ['protocol-ref'], 'api-path': 'api-ref', }) .parseSync(); @@ -63,18 +49,8 @@ const argv = yargs(process.argv.slice(2)) (async () => { try { const outPath = 'lib/src/vendor'; - const platform = nodePlatformToDartPlatform( - process.env.npm_config_platform || process.platform - ); - const arch = nodeArchToDartArch( - process.env.npm_config_arch || process.arch - ); - if (argv['protocol-version']) { - await getEmbeddedProtocol(outPath, { - version: argv['protocol-version'], - }); - } else if (argv['protocol-ref']) { + if (argv['protocol-ref']) { await getEmbeddedProtocol(outPath, { ref: argv['protocol-ref'], }); @@ -87,20 +63,16 @@ const argv = yargs(process.argv.slice(2)) } if (!argv['skip-compiler']) { - if (argv['compiler-version']) { - await getDartSassEmbedded(outPath, platform, arch, { - version: argv['compiler-version'], - }); - } else if (argv['compiler-ref']) { - await getDartSassEmbedded(outPath, platform, arch, { + if (argv['compiler-ref']) { + await getEmbeddedCompiler(outPath, { ref: argv['compiler-ref'], }); } else if (argv['compiler-path']) { - await getDartSassEmbedded(outPath, platform, arch, { + await getEmbeddedCompiler(outPath, { path: argv['compiler-path'], }); } else { - await getDartSassEmbedded(outPath, platform, arch); + await getEmbeddedCompiler(outPath); } } diff --git a/tool/prepare-optional-release.ts b/tool/prepare-optional-release.ts index d1d71fe0..1847d1fd 100644 --- a/tool/prepare-optional-release.ts +++ b/tool/prepare-optional-release.ts @@ -1,11 +1,15 @@ +import extractZip = require('extract-zip'); +import {promises as fs} from 'fs'; +import fetch from 'node-fetch'; +import * as p from 'path'; +import {extract as extractTar} from 'tar'; import yargs from 'yargs'; -import { - getDartSassEmbedded, - nodePlatformToDartPlatform, - nodeArchToDartArch, -} from './utils'; import * as pkg from '../package.json'; +import * as utils from './utils'; + +export type DartPlatform = 'linux' | 'macos' | 'windows'; +export type DartArch = 'ia32' | 'x64' | 'arm' | 'arm64'; const argv = yargs(process.argv.slice(2)) .option('package', { @@ -19,13 +23,112 @@ const argv = yargs(process.argv.slice(2)) }) .parseSync(); +// Converts a Node-style platform name as returned by `process.platform` into a +// name used by Dart Sass. Throws if the operating system is not supported by +// Dart Sass Embedded. +export function nodePlatformToDartPlatform(platform: string): DartPlatform { + switch (platform) { + case 'linux': + return 'linux'; + case 'darwin': + return 'macos'; + case 'win32': + return 'windows'; + default: + throw Error(`Platform ${platform} is not supported.`); + } +} + +// Converts a Node-style architecture name as returned by `process.arch` into a +// name used by Dart Sass. Throws if the architecture is not supported by Dart +// Sass Embedded. +export function nodeArchToDartArch(arch: string): DartArch { + switch (arch) { + case 'ia32': + return 'ia32'; + case 'x86': + return 'ia32'; + case 'x64': + return 'x64'; + case 'arm': + return 'arm'; + case 'arm64': + return 'arm64'; + default: + throw Error(`Architecture ${arch} is not supported.`); + } +} + +// Get the platform's file extension for archives. +function getArchiveExtension(platform: DartPlatform): '.zip' | '.tar.gz' { + return platform === 'windows' ? '.zip' : '.tar.gz'; +} + +// Downloads the release for `repo` located at `assetUrl`, then unzips it into +// `outPath`. +async function downloadRelease(options: { + repo: string; + assetUrl: string; + outPath: string; +}): Promise { + console.log(`Downloading ${options.repo} release asset.`); + const response = await fetch(options.assetUrl, { + redirect: 'follow', + }); + if (!response.ok) { + throw Error( + `Failed to download ${options.repo} release asset: ${response.statusText}` + ); + } + const releaseAsset = await response.buffer(); + + console.log(`Unzipping ${options.repo} release asset to ${options.outPath}.`); + await utils.cleanDir(p.join(options.outPath, options.repo)); + + const archiveExtension = options.assetUrl.endsWith('.zip') + ? '.zip' + : '.tar.gz'; + const zippedAssetPath = + options.outPath + '/' + options.repo + archiveExtension; + await fs.writeFile(zippedAssetPath, releaseAsset); + if (archiveExtension === '.zip') { + await extractZip(zippedAssetPath, { + dir: p.join(process.cwd(), options.outPath), + }); + } else { + extractTar({ + file: zippedAssetPath, + cwd: options.outPath, + sync: true, + }); + } + await fs.unlink(zippedAssetPath); +} + (async () => { try { - const [platform, arch] = argv.package.split('-'); - await getDartSassEmbedded( - `npm/${argv.package}`, - nodePlatformToDartPlatform(platform), - nodeArchToDartArch(arch) + const version = pkg['compiler-version'] as string; + if (version.endsWith('-dev')) { + throw Error( + "Can't release optional packages for a -dev compiler version." + ); + } + + const [nodePlatform, nodeArch] = argv.package.split('-'); + const dartPlatform = nodePlatformToDartPlatform(nodePlatform); + const dartArch = nodeArchToDartArch(nodeArch); + const outPath = p.join('npm', argv.package); + await downloadRelease({ + repo: 'dart-sass-embedded', + assetUrl: + 'https://github.com/sass/dart-sass-embedded/releases/download/' + + `${version}/sass_embedded-${version}-` + + `${dartPlatform}-${dartArch}${getArchiveExtension(dartPlatform)}`, + outPath, + }); + await fs.rename( + p.join(outPath, 'sass_embedded'), + p.join(outPath, 'dart-sass-embedded') ); } catch (error) { console.error(error); diff --git a/tool/prepare-release.ts b/tool/prepare-release.ts index 885f4ed6..20de61d8 100644 --- a/tool/prepare-release.ts +++ b/tool/prepare-release.ts @@ -6,9 +6,8 @@ import {promises as fs} from 'fs'; import * as shell from 'shelljs'; import * as pkg from '../package.json'; -import {getEmbeddedProtocol, getJSApi} from './utils'; - -shell.config.fatal = true; +import {getEmbeddedProtocol} from './get-embedded-protocol'; +import {getJSApi} from './get-js-api'; (async () => { try { diff --git a/tool/utils.ts b/tool/utils.ts index 7f27de42..2c6fbd0a 100644 --- a/tool/utils.ts +++ b/tool/utils.ts @@ -2,281 +2,17 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import extractZip = require('extract-zip'); -import {promises as fs, existsSync, mkdirSync} from 'fs'; -import fetch from 'node-fetch'; +import {promises as fs, existsSync} from 'fs'; import * as p from 'path'; -import * as yaml from 'yaml'; import * as shell from 'shelljs'; -import {extract as extractTar} from 'tar'; - -import * as pkg from '../package.json'; shell.config.fatal = true; // Directory that holds source files. -const BUILD_PATH = 'build'; - -export type DartPlatform = 'linux' | 'macos' | 'windows'; -export type DartArch = 'ia32' | 'x64' | 'arm' | 'arm64'; - -// Converts a Node-style platform name as returned by `process.platform` into a -// name used by Dart Sass. Throws if the operating system is not supported by -// Dart Sass Embedded. -export function nodePlatformToDartPlatform(platform: string): DartPlatform { - switch (platform) { - case 'linux': - return 'linux'; - case 'darwin': - return 'macos'; - case 'win32': - return 'windows'; - default: - throw Error(`Platform ${platform} is not supported.`); - } -} - -// Converts a Node-style architecture name as returned by `process.arch` into a -// name used by Dart Sass. Throws if the architecture is not supported by Dart -// Sass Embedded. -export function nodeArchToDartArch(arch: string): DartArch { - switch (arch) { - case 'ia32': - return 'ia32'; - case 'x86': - return 'ia32'; - case 'x64': - return 'x64'; - case 'arm': - return 'arm'; - case 'arm64': - return 'arm64'; - default: - throw Error(`Architecture ${arch} is not supported.`); - } -} - -// Get the platform's file extension for archives. -function getArchiveExtension(platform: DartPlatform): '.zip' | '.tar.gz' { - return platform === 'windows' ? '.zip' : '.tar.gz'; -} - -/** - * Gets the Embedded Protocol. - * - * Can download the release `version`, check out and build the source from a Git - * `ref`, or build from the source at `path`. - * - * By default, downloads the release version specified in package.json. Throws - * if an error occurs. - */ -export async function getEmbeddedProtocol( - outPath: string, - options?: - | { - version: string; - } - | { - ref: string; - } - | { - path: string; - } -): Promise { - const repo = 'embedded-protocol'; - - options ??= defaultVersionOption('protocol-version'); - if ('version' in options) { - const version = options?.version; - await downloadRelease({ - repo, - assetUrl: `https://github.com/sass/${repo}/archive/${version}.tar.gz`, - outPath: BUILD_PATH, - }); - await fs.rename( - p.join(BUILD_PATH, `${repo}-${version}`), - p.join(BUILD_PATH, repo) - ); - } else if ('ref' in options) { - fetchRepo({ - repo, - outPath: BUILD_PATH, - ref: options.ref, - }); - } - - const source = - options && 'path' in options ? options.path : p.join(BUILD_PATH, repo); - buildEmbeddedProtocol(source); - await link('build/embedded-protocol', p.join(outPath, repo)); -} - -/** - * Gets the Dart Sass wrapper for the Embedded Compiler. - * - * Can download the release `version`, check out and build the source from a Git - * `ref`, or build from the source at `path`. - * - * By default, downloads the release version specified in package.json. Throws - * if an error occurs. - */ -export async function getDartSassEmbedded( - outPath: string, - platform: DartPlatform, - arch: DartArch, - options?: - | { - version: string; - } - | { - ref: string; - } - | { - path: string; - } -): Promise { - const repo = 'dart-sass-embedded'; - options ??= defaultVersionOption('compiler-version'); - - await checkForMusl(); - - if ('version' in options) { - const version = options?.version; - await downloadRelease({ - repo, - assetUrl: - `https://github.com/sass/${repo}/releases/download/` + - `${version}/sass_embedded-${version}-` + - `${platform}-${arch}${getArchiveExtension(platform)}`, - outPath, - }); - await fs.rename(p.join(outPath, 'sass_embedded'), p.join(outPath, repo)); - return; - } - - if ('ref' in options) { - fetchRepo({ - repo, - outPath: BUILD_PATH, - ref: options.ref, - }); - await maybeOverrideSassDependency(repo); - } - - const source = 'path' in options ? options.path : p.join(BUILD_PATH, repo); - buildDartSassEmbedded(source); - await link(p.join(source, 'build'), p.join(outPath, repo)); -} - -/** - * Overrides the dart-sass dependency to latest git commit on main when the - * pubspec file declares it as a "-dev" dependency. - */ -async function maybeOverrideSassDependency(repo: string): Promise { - const pubspecPath = p.join(BUILD_PATH, repo, 'pubspec.yaml'); - const pubspecRaw = await fs.readFile(pubspecPath, {encoding: 'utf-8'}); - const pubspec = yaml.parse(pubspecRaw); - const sassVersion = pubspec?.dependencies?.sass; - - if (typeof sassVersion !== 'string') return; - if (!sassVersion.endsWith('-dev')) return; - - console.log( - `${repo} depends on sass: ${sassVersion}, overriding with latest commit.` - ); - - pubspec['dependency_overrides'] = { - ...pubspec['dependency_overrides'], - sass: {git: 'https://github.com/sass/dart-sass.git'}, - }; - await fs.writeFile(pubspecPath, yaml.stringify(pubspec), {encoding: 'utf-8'}); -} - -/** - * Throws an informative error if we're running in a Linux environment that uses - * musl. - */ -async function checkForMusl(): Promise { - if (process.platform !== 'linux') return; - - const executable = await fs.readFile(process.execPath); - if (!executable.includes('libc.musl-')) return; - - throw Error( - "sass-embedded doesn't support Linux distributions that use musl-libc." - ); -} - -/** - * Checks out JS API type definitions from the Sass language repo. - * - * Can check out a Git `ref`, or link to the source at `path`. By default, - * checks out the latest revision from GitHub. - */ -export async function getJSApi( - outPath: string, - options?: {ref: string} | {path: string} -): Promise { - const repo = 'sass'; - - let source: string; - if (!options || 'ref' in options) { - fetchRepo({ - repo, - outPath: BUILD_PATH, - ref: options?.ref ?? 'main', - }); - source = p.join(BUILD_PATH, repo); - } else { - source = options.path; - } - - await link(p.join(source, 'js-api-doc'), p.join(outPath, repo)); -} - -// Downloads the release for `repo` located at `assetUrl`, then unzips it into -// `outPath`. -async function downloadRelease(options: { - repo: string; - assetUrl: string; - outPath: string; -}): Promise { - console.log(`Downloading ${options.repo} release asset.`); - const response = await fetch(options.assetUrl, { - redirect: 'follow', - }); - if (!response.ok) { - throw Error( - `Failed to download ${options.repo} release asset: ${response.statusText}` - ); - } - const releaseAsset = await response.buffer(); - - console.log(`Unzipping ${options.repo} release asset to ${options.outPath}.`); - await cleanDir(p.join(options.outPath, options.repo)); - - const archiveExtension = options.assetUrl.endsWith('.zip') - ? '.zip' - : '.tar.gz'; - const zippedAssetPath = - options.outPath + '/' + options.repo + archiveExtension; - await fs.writeFile(zippedAssetPath, releaseAsset); - if (archiveExtension === '.zip') { - await extractZip(zippedAssetPath, { - dir: p.join(process.cwd(), options.outPath), - }); - } else { - extractTar({ - file: zippedAssetPath, - cwd: options.outPath, - sync: true, - }); - } - await fs.unlink(zippedAssetPath); -} +export const BUILD_PATH = 'build'; // Clones `repo` into `outPath`, then checks out the given Git `ref`. -function fetchRepo(options: { +export function fetchRepo(options: { repo: string; outPath: string; ref: string; @@ -304,62 +40,8 @@ function fetchRepo(options: { ); } -// Builds the embedded proto at `repoPath` into a pbjs with TS declaration file. -function buildEmbeddedProtocol(repoPath: string): void { - const proto = p.join(repoPath, 'embedded_sass.proto'); - const protocPath = - process.platform === 'win32' - ? '%CD%/node_modules/protoc/protoc/bin/protoc.exe' - : 'node_modules/protoc/protoc/bin/protoc'; - const version = shell - .exec(`${protocPath} --version`, {silent: true}) - .stdout.trim(); - console.log( - `Building pbjs and TS declaration file from ${proto} with ${version}.` - ); - - const pluginPath = - process.platform === 'win32' - ? '%CD%/node_modules/.bin/protoc-gen-ts.cmd' - : 'node_modules/.bin/protoc-gen-ts'; - mkdirSync('build/embedded-protocol', {recursive: true}); - shell.exec( - `${protocPath} \ - --plugin="protoc-gen-ts=${pluginPath}" \ - --js_out="import_style=commonjs,binary:build/embedded-protocol" \ - --ts_out="build/embedded-protocol" \ - --proto_path="${repoPath}" \ - ${proto}`, - {silent: true} - ); -} - -// Builds the Embedded Dart Sass executable from the source at `repoPath`. -function buildDartSassEmbedded(repoPath: string): void { - console.log('Downloading dart-sass-embedded dependencies.'); - shell.exec('dart pub upgrade', { - cwd: repoPath, - silent: true, - }); - - console.log('Building dart-sass-embedded executable.'); - shell.exec('dart run grinder protobuf pkg-standalone-dev', { - cwd: repoPath, - silent: true, - }); -} - -// Given the name of a field in `package.json`, returns the default version -// option described by that field. -function defaultVersionOption( - pkgField: keyof typeof pkg -): {version: string} | {ref: string} { - const version = pkg[pkgField] as string; - return version.endsWith('-dev') ? {ref: 'main'} : {version}; -} - // Links or copies the contents of `source` into `destination`. -async function link(source: string, destination: string): Promise { +export async function link(source: string, destination: string): Promise { await cleanDir(destination); if (process.platform === 'win32') { console.log(`Copying ${source} into ${destination}.`); @@ -372,7 +54,7 @@ async function link(source: string, destination: string): Promise { } // Ensures that `dir` does not exist, but its parent directory does. -async function cleanDir(dir: string): Promise { +export async function cleanDir(dir: string): Promise { await fs.mkdir(p.dirname(dir), {recursive: true}); try { await fs.rmdir(dir, {recursive: true});