Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion code/code-schematics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"build:library": "yarn library build",
"build:schematic-factory": "yarn node ./src/scripts/schematic-factory-build.script.ts",
"prepack": "yarn run build",
"postpack": "rm -rf dist"
"postpack": "rm -rf dist",
"smoke:schematic": "yarn node ./src/scripts/schematic-smoke.script.ts"
},
"dependencies": {
"@angular-devkit/core": "19.1.5",
Expand Down
240 changes: 240 additions & 0 deletions code/code-schematics/src/scripts/schematic-smoke.script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/* eslint-disable no-await-in-loop, no-console */

import type { PortablePath } from '@yarnpkg/fslib'

import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

import { prepareTmpDir } from '../helpers/index.js'
import { runSchematicHelper } from '../helpers/index.js'
import { writeTmpSchematicHelper } from '../helpers/index.js'

const helperScanRoots = [
'package.json',
'scripts/raijin',
'README.md',
'README_EN.md',
'code/code-schematics/src/scripts/schematic-smoke.script.ts',
'docs/README.md',
'docs/README.ru.md',
'docs/raijin/README.md',
'docs/raijin/README.ru.md',
'docs/raijin/quickstart.md',
'docs/raijin/quickstart.ru.md',
'docs/raijin/commands.md',
'docs/raijin/commands.ru.md',
]

const requiredGeneratedFiles = [
'.gitignore',
'.prettierrc.mjs',
'.github/workflows/checks.yaml',
'.github/workflows/preview.yaml',
'.github/workflows/release.yaml',
'tsconfig.json',
]

const readJson = async <T>(filePath: string): Promise<T> =>
JSON.parse(await fs.readFile(filePath, 'utf8')) as T

const pathExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}

const findRepoRoot = async (startDir: string): Promise<string> => {
let currentDir = startDir

while (currentDir !== path.dirname(currentDir)) {
if (await pathExists(path.join(currentDir, 'docs/raijin/index.v1.json'))) {
return currentDir
}

currentDir = path.dirname(currentDir)
}

throw new Error('Cannot find repository root with docs/raijin/index.v1.json')
}

const walkFiles = async (entryPath: string): Promise<Array<string>> => {
if (!(await pathExists(entryPath))) return []

const stat = await fs.stat(entryPath)

if (stat.isFile()) return [entryPath]

const entries = await fs.readdir(entryPath, { withFileTypes: true })
const nested = await Promise.all(
entries.map(async (entry) => walkFiles(path.join(entryPath, entry.name)))
)

return nested.flat()
}

const assertInactiveCommandsAreNotInvoked = async (repoRoot: string): Promise<void> => {
type CommandIndex = {
commands: Array<{
command: string
status: string
}>
}

const index = await readJson<CommandIndex>(path.join(repoRoot, 'docs/raijin/index.v1.json'))
const inactiveCommands = index.commands
.filter((command) => command.status === 'inactive')
.map((command) => command.command)

const scanFiles = (
await Promise.all(
helperScanRoots.map(async (scanRoot) => walkFiles(path.join(repoRoot, scanRoot)))
)
)
.flat()
.filter((filePath) =>
['.json', '.js', '.mjs', '.ts', '.tsx', '.md'].includes(path.extname(filePath)))

const violations: Array<string> = []

for (const filePath of scanFiles) {
const content = await fs.readFile(filePath, 'utf8')
const relativePath = path.relative(repoRoot, filePath)

for (const command of inactiveCommands) {
const invocation = ['yarn', command].join(' ')

if (content.includes(invocation)) {
violations.push(`${relativePath}: uses inactive command "${invocation}"`)
}
}
}

if (violations.length > 0) {
throw new Error(`Inactive command invocations found:\n${violations.join('\n')}`)
}
}

const writeFixturePackage = async (fixtureDir: string): Promise<void> => {
const packageJson = {
name: 'raijin-schematic-smoke-fixture',
private: true,
type: 'module',
}

await fs.writeFile(
path.join(fixtureDir, 'package.json'),
`${JSON.stringify(packageJson, null, 2)}\n`
)
await fs.writeFile(path.join(fixtureDir, 'tsconfig.json'), `${JSON.stringify({}, null, 2)}\n`)
}

const prepareCollectionDir = async (repoRoot: string, collectionDir: string): Promise<void> => {
const previousCwd = process.cwd()
const collectionPortablePath = collectionDir as PortablePath

try {
process.chdir(repoRoot)
await prepareTmpDir(collectionPortablePath)
} finally {
process.chdir(previousCwd)
}
}

const runProjectSchematic = async ({
collectionPath,
fixtureDir,
}: {
collectionPath: string
fixtureDir: string
}): Promise<void> => {
const previousCwd = process.cwd()

try {
process.chdir(fixtureDir)

const exitCode = await runSchematicHelper(
'project',
{
type: 'project',
cwd: fixtureDir,
},
collectionPath
)

if (exitCode !== 0) {
throw new Error(`Schematic workflow failed with exit code ${exitCode}`)
}
} finally {
process.chdir(previousCwd)
}
}

const assertGeneratedFixture = async (fixtureDir: string): Promise<void> => {
const missingFiles: Array<string> = []

for (const relativePath of requiredGeneratedFiles) {
if (!(await pathExists(path.join(fixtureDir, relativePath)))) {
missingFiles.push(relativePath)
}
}

if (missingFiles.length > 0) {
throw new Error(`Schematic smoke did not generate required files:\n${missingFiles.join('\n')}`)
}

const gitignore = await fs.readFile(path.join(fixtureDir, '.gitignore'), 'utf8')

if (!gitignore.includes('node_modules') || !gitignore.includes('dist/')) {
throw new Error('Generated .gitignore does not contain expected baseline entries')
}

const tsconfig = await readJson<{ compilerOptions?: unknown }>(
path.join(fixtureDir, 'tsconfig.json')
)

if (!tsconfig.compilerOptions || typeof tsconfig.compilerOptions !== 'object') {
throw new Error('Generated tsconfig.json does not contain compilerOptions')
}
}

const runSchematicSmoke = async (): Promise<void> => {
const repoRoot = await findRepoRoot(process.cwd())

await assertInactiveCommandsAreNotInvoked(repoRoot)

const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'raijin-schematic-smoke-'))
const collectionDir = path.join(tmpRoot, 'collection')
const fixtureDir = path.join(tmpRoot, 'fixture')

try {
await fs.mkdir(collectionDir, { recursive: true })
await fs.mkdir(fixtureDir, { recursive: true })
await writeFixturePackage(fixtureDir)
await writeTmpSchematicHelper(collectionDir as PortablePath)
await prepareCollectionDir(repoRoot, collectionDir)
await runProjectSchematic({
collectionPath: path.join(collectionDir, 'collection.json'),
fixtureDir,
})
await assertGeneratedFixture(fixtureDir)
} finally {
await fs.rm(tmpRoot, { recursive: true, force: true })
}
}

try {
await runSchematicSmoke()
console.log('Schematic smoke passed')
} catch (error) {
if (error instanceof Error) {
console.error(error.message)
} else {
console.error(error)
}

process.exitCode = 1
}
3 changes: 2 additions & 1 deletion docs/raijin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ Navigation for custom `atls` Yarn bundle docs

- `yarn raijin:generate`
- `yarn raijin:check`
- `yarn schematic:test`

<!-- sync:router-coverage -->

## Coverage snapshot

- Commands: 36 (active: 35, inactive: 1)
- Workspace packages: 72
- Last generated: 2026-04-29T23:07:25.462Z
- Last generated: 2026-04-30T02:04:56.641Z
3 changes: 2 additions & 1 deletion docs/raijin/README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@

- `yarn raijin:generate`
- `yarn raijin:check`
- `yarn schematic:test`

<!-- sync:router-coverage -->

## Покрытие текущей версии

- Команд: 36 (active: 35, inactive: 1)
- Workspace-пакетов: 72
- Последняя генерация: 2026-04-29T23:07:25.462Z
- Последняя генерация: 2026-04-30T02:04:56.641Z
2 changes: 1 addition & 1 deletion docs/raijin/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ Command map extracted from `yarn/plugin-*` and `@atls/yarn-cli` bundle
- Status: `inactive`
- Purpose: Generate a new project from schematics templates.
- When to use: Use when scaffolding a new project structure from predefined templates.
- Example: `yarn generate project`
- Example: unavailable for inactive command
- Plugin: `@atls/yarn-plugin-schematics`
- Source: `yarn/plugin-schematics/sources/commands/generate-project.command.tsx`
- Routing: do not use (plugin is in bundle but not exported from plugin index)
Expand Down
2 changes: 1 addition & 1 deletion docs/raijin/commands.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@
- Статус: `inactive`
- Назначение: Сгенерировать новый проект из шаблонов schematics.
- Когда использовать: Используйте при создании новой структуры проекта из готовых шаблонов.
- Пример: `yarn generate project`
- Пример: недоступен для inactive-команды
- Плагин: `@atls/yarn-plugin-schematics`
- Исходник: `yarn/plugin-schematics/sources/commands/generate-project.command.tsx`
- Маршрутизация: не использовать (plugin is in bundle but not exported from plugin index)
Expand Down
4 changes: 2 additions & 2 deletions docs/raijin/index.meta.v1.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"generatedBy": "scripts/raijin/generate-artifacts.mjs",
"contentSha256": "65af7f2f92cbb25b7c6ffc03f74fef03ce3b6c3ada0bb1b2e2e48bbf10b18160",
"contentSha256": "ea3a439834e83cb928e43f7904dcf691899b5e05f7a0ae3fc3087d26b15cc1b3",
"packageManager": "yarn@4.14.1",
"workspaceCount": 72,
"commandCount": 36,
Expand All @@ -10,5 +10,5 @@
"semanticsSchemaVersion": 1,
"semanticsCommandCount": 36,
"semanticsWorkspaceCount": 72,
"lastGenerated": "2026-04-29T23:07:25.462Z"
"lastGenerated": "2026-04-30T02:04:56.641Z"
}
11 changes: 9 additions & 2 deletions docs/raijin/index.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,14 @@
"description": "",
"purposeEn": "Code utility library for raijin workflows",
"purposeRu": "Библиотека code-утилит для raijin-сценариев",
"scripts": ["build", "build:library", "build:schematic-factory", "postpack", "prepack"],
"scripts": [
"build",
"build:library",
"build:schematic-factory",
"postpack",
"prepack",
"smoke:schematic"
],
"dependencyCount": 4,
"devDependencyCount": 8,
"peerDependencyCount": 2
Expand Down Expand Up @@ -1482,5 +1489,5 @@
}
]
},
"lastGenerated": "2026-04-29T23:07:25.462Z"
"lastGenerated": "2026-04-30T02:04:56.641Z"
}
2 changes: 1 addition & 1 deletion docs/raijin/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ Compact list:
- When to use: Use when building templates, generators, or schematic factories.
- Example: `Add a schematic that scaffolds a new workspace package.`
- Tags: `code`
- Scripts: `build`, `build:library`, `build:schematic-factory`, `postpack`, `prepack`
- Scripts: `build`, `build:library`, `build:schematic-factory`, `postpack`, `prepack`, `smoke:schematic`
- Dependencies: deps 4, devDeps 8, peerDeps 2

<!-- sync:package-card:atls-code-service -->
Expand Down
2 changes: 1 addition & 1 deletion docs/raijin/packages.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@
- Когда использовать: Используйте при создании шаблонов, генераторов или фабрик схем.
- Пример: `Добавить схему, которая создаёт новый workspace-пакет.`
- Теги: `code`
- Скрипты: `build`, `build:library`, `build:schematic-factory`, `postpack`, `prepack`
- Скрипты: `build`, `build:library`, `build:schematic-factory`, `postpack`, `prepack`, `smoke:schematic`
- Зависимости: deps 4, devDeps 8, peerDeps 2

<!-- sync:package-card:atls-code-service -->
Expand Down
15 changes: 14 additions & 1 deletion docs/raijin/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,22 @@ Expected result:
- `yarn check` runs a complete validation pass without routing errors
- `yarn files changed list` returns file list (or empty list if no changes)

<!-- sync:schematic-smoke -->

## 5. Local schematics smoke check

```bash
yarn schematic:test
```

Expected result:

- Temporary fixture is created through public `@atls/code-schematics` exports
- Check fails if helper or Markdown docs invoke an inactive command

<!-- sync:consumer-howto -->

## 5. How to use in an external project
## 6. How to use in an external project

- Install once, then keep it current with `yarn set version atls`
- Commit `.yarn/releases` and `.yarnrc.yml` changes together with bundle updates
Expand Down
Loading
Loading