diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6a93ff78..e5f0ea55 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -25,6 +25,8 @@ jobs: - name: Build if: success() run: npm run build + - name: Check demo language files + run: test -f dist/demo/assets/lang/en-US.lang && test -f dist/demo/assets/lang/de-DE.lang - name: Deploy - https://draggable.github.io/formeo/ if: success() uses: peaceiris/actions-gh-pages@v4 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 931a75b0..df16f6fe 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -27,6 +27,8 @@ jobs: run: npm test - name: Build run: npm run build + - name: Check demo language files + run: test -f dist/demo/assets/lang/en-US.lang && test -f dist/demo/assets/lang/de-DE.lang - name: Test built package run: npm run test:dist - name: Publish diff --git a/src/lib/js/copy-assets.test.mjs b/src/lib/js/copy-assets.test.mjs new file mode 100644 index 00000000..f3be263e --- /dev/null +++ b/src/lib/js/copy-assets.test.mjs @@ -0,0 +1,53 @@ +import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { suite, test } from 'node:test' +import { copyTargets, targets } from '../../../tools/copy-assets.mjs' + +const fixture = (t, files = {}) => { + const dir = mkdtempSync(join(tmpdir(), 'formeo-copy-assets-')) + t.after(() => rmSync(dir, { recursive: true, force: true })) + for (const [path, text] of Object.entries(files)) { + mkdirSync(join(dir, path, '..'), { recursive: true }) + writeFileSync(join(dir, path), text) + } + return dir +} + +suite('copy-assets', () => { + test('copies nested files flat by basename and skips directories', async t => { + const src = fixture(t, { 'lang/de-DE.lang': 'de', 'lang/de-DE.json': '{}', 'top.lang': 'top' }) + const dest = join(fixture(t), 'out') + + t.assert.strictEqual(await copyTargets([{ src: join(src, '**/*'), dest }]), 0) + t.assert.deepStrictEqual(readdirSync(dest).sort(), ['de-DE.json', 'de-DE.lang', 'top.lang']) + }) + + test('the language target copies the installed package files into the demo', async t => { + const langTarget = targets.find(({ dest }) => dest.endsWith(join('assets', 'lang'))) + const dest = fixture(t) + + t.assert.strictEqual(await copyTargets([{ ...langTarget, dest }]), 0) + const files = readdirSync(dest) + t.assert.ok(files.includes('en-US.lang')) + t.assert.ok(files.includes('de-DE.lang')) + t.assert.ok(!files.includes('lang')) + }) + + test('*.js does not pick up .cjs files', async t => { + const src = fixture(t, { 'formeo.es.js': '', 'formeo.cjs': '' }) + const dest = join(fixture(t), 'out') + + await copyTargets([{ src: join(src, '*.js'), dest }]) + t.assert.deepStrictEqual(readdirSync(dest), ['formeo.es.js']) + }) + + test('counts a failed copy instead of swallowing it', async t => { + t.mock.method(console, 'error', () => {}) + const src = fixture(t, { 'a.lang': 'a' }) + const dest = join(fixture(t, { 'not-a-dir': '' }), 'not-a-dir') + + t.assert.strictEqual(await copyTargets([{ src: join(src, '*'), dest }]), 1) + t.assert.strictEqual(console.error.mock.callCount(), 1) + }) +}) diff --git a/tools/copy-assets.mjs b/tools/copy-assets.mjs index 3652e5a7..e536eb36 100644 --- a/tools/copy-assets.mjs +++ b/tools/copy-assets.mjs @@ -1,13 +1,13 @@ // copyDir.mjs import { promises as fs } from 'node:fs' import { basename, dirname, join, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' +import { fileURLToPath, pathToFileURL } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const projectRoot = resolve(__dirname, '..') -const targets = [ +export const targets = [ { src: resolve(projectRoot, 'src/lib/icons/formeo-sprite.svg'), dest: resolve(projectRoot, 'dist/demo/assets/img/'), @@ -17,7 +17,8 @@ const targets = [ dest: resolve(projectRoot, 'dist/'), }, { - src: resolve(projectRoot, 'node_modules', '@draggable/formeo-languages/dist/lang/*'), + // Any depth: since 3.5.1 the package nests its files in dist/lang/lang/. They are copied flat by basename. + src: resolve(projectRoot, 'node_modules', '@draggable/formeo-languages/dist/lang/**/*'), dest: resolve(projectRoot, 'dist/demo/assets/lang'), }, { @@ -37,22 +38,29 @@ const targets = [ async function copyFile(src, dest, rename = null) { for await (const file of fs.glob(src)) { + if ((await fs.stat(file)).isDirectory()) continue const destPath = rename ? join(dest, rename) : join(dest, basename(file)) await fs.mkdir(dirname(destPath), { recursive: true }) await fs.copyFile(file, destPath) } } -// Entry point -async function main() { +/** Copy every target, carrying on past errors; returns the number of targets that failed. */ +export async function copyTargets(targets) { + let failures = 0 for (const target of targets) { try { await copyFile(target.src, target.dest, target.rename) console.log(`Copied ${basename(target.src)} to ${target.dest}`) } catch (error) { + failures++ console.error(`Error copying file: ${error.message}`) } } + return failures } -main() +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + // A failed copy fails the build, so a broken demo can't be deployed silently. + if (await copyTargets(targets)) process.exitCode = 1 +}